3"""Authenticate the canonical bench holder against Linux kernel state."""
5from __future__
import annotations
20from collections.abc
import Callable
21from dataclasses
import dataclass, replace
22from pathlib
import Path
23from unittest
import mock
25CANONICAL_ROOT = Path(
"/var/lib/ra8-bench")
26LOCK_ID_RE = re.compile(
r"[0-9a-f]{16}")
27SHA256_RE = re.compile(
r"[0-9a-f]{64}")
28MAX_RECORD_BYTES = 8192
29MAX_SCRIPT_BYTES = 128 * 1024
31SECOND_LOCK_OBSERVATION = 2
34class LockProofError(ValueError):
35 """The advertised holder did not match the live canonical flock."""
38class _ProofDeadlineError(RuntimeError):
39 """A selftest descriptor proof exceeded its hard liveness deadline."""
42@dataclass(frozen=
True)
44 """Expected identities and local authorities for one live proof."""
48 expected_script_sha256: str
49 expected_broker_sha256: str
50 root: Path = CANONICAL_ROOT
51 proc_root: Path = Path(
"/proc")
54@dataclass(frozen=True)
56 """Descriptor-bound identities retained across the two proof phases."""
58 record: dict[str, object]
59 record_identity: os.stat_result
60 lock_identity: os.stat_result
64@dataclass(frozen=True)
66 """Paths and identities for one synthetic proc/lock authority."""
81 def digest(self) -> str:
82 """Return the reviewed holder fixture digest."""
83 return hashlib.sha256(self.script.read_bytes()).hexdigest()
86 def broker_digest(self) -> str:
87 """Return the reviewed broker fixture digest."""
88 return hashlib.sha256(self.broker.read_bytes()).hexdigest()
91 def request(self) -> ProofRequest:
92 """Return the valid proof request bound to this fixture."""
106 post_read: Callable[[],
None] |
None =
None,
107) -> tuple[bytes, os.stat_result]:
108 """Read one non-linked regular file through one stable descriptor."""
109 flags = os.O_RDONLY | os.O_CLOEXEC | getattr(os,
"O_NOFOLLOW", 0)
111 descriptor = os.open(path, flags)
112 except OSError
as exc:
113 msg = f
"cannot open {path}: {exc}"
114 raise LockProofError(msg)
from exc
116 before = os.fstat(descriptor)
117 if not stat.S_ISREG(before.st_mode):
118 msg = f
"{path} is not a regular file"
119 raise LockProofError(msg)
120 raw = os.read(descriptor, limit + 1)
121 if post_read
is not None:
123 after = os.fstat(descriptor)
126 if len(raw) > limit
or (before.st_dev, before.st_ino) != (
130 msg = f
"{path} changed or exceeded its size bound"
131 raise LockProofError(msg)
133 current = path.stat(follow_symlinks=
False)
134 except OSError
as exc:
135 msg = f
"cannot restat {path}: {exc}"
136 raise LockProofError(msg)
from exc
137 if (current.st_dev, current.st_ino) != (before.st_dev, before.st_ino):
138 msg = f
"{path} was replaced during authentication"
139 raise LockProofError(msg)
143def _record(path: Path, lock_id: str, kind: str) -> tuple[dict[str, object], os.stat_result]:
144 """Load and validate the holder record's capability-binding fields."""
145 raw, identity = _read_regular(path, MAX_RECORD_BYTES)
147 value = json.loads(raw.decode(
"utf-8",
"strict"))
148 except (UnicodeError, json.JSONDecodeError)
as exc:
149 msg =
"holder record is malformed"
150 raise LockProofError(msg)
from exc
151 if not isinstance(value, dict):
152 msg =
"holder record is not a mapping"
153 raise LockProofError(msg)
164 if not required <= value.keys():
165 msg =
"holder record omits live-capability fields"
166 raise LockProofError(msg)
167 if value[
"resource"] !=
"bench" or value[
"lock_id"] != lock_id:
168 msg =
"holder record does not name this bench transaction"
169 raise LockProofError(msg)
170 if value[
"hold_kind"] != kind:
171 msg =
"holder record has the wrong hold kind"
172 raise LockProofError(msg)
173 if type(value[
"pid"])
is not int
or int(value[
"pid"]) <= 1:
174 msg =
"holder record PID is invalid"
175 raise LockProofError(msg)
176 for field
in (
"pid_start_ticks",
"host_pid",
"host_start_ticks"):
177 if type(value[field])
is not int
or int(value[field]) <= 0:
178 msg = f
"holder record {field} is invalid"
179 raise LockProofError(msg)
180 return value, identity
183def _start_ticks(proc_root: Path, pid: int) -> int:
184 """Read one process start time without confusing spaces in comm."""
186 raw = (proc_root / str(pid) /
"stat").read_text(encoding=
"ascii")
187 tail = raw[raw.rindex(
")") + 2 :].split()
189 except (OSError, ValueError, IndexError)
as exc:
190 msg =
"cannot authenticate holder PID start time"
191 raise LockProofError(msg)
from exc
194def _lock_owner_pids(proc_root: Path, lock_stat: os.stat_result) -> set[int]:
195 """Return active kernel FLOCK owners for one exact device and inode."""
197 lines = (proc_root /
"locks").read_text(encoding=
"ascii").splitlines()
198 except OSError
as exc:
199 msg =
"cannot read kernel lock authority"
200 raise LockProofError(msg)
from exc
201 owners: set[int] = set()
203 fields = line.split()
204 if "FLOCK" not in fields
or "->" in fields:
206 for index, field
in enumerate(fields):
207 match = re.fullmatch(
r"([0-9a-fA-F]+):([0-9a-fA-F]+):(\d+)", field)
208 if match
is None or index == 0
or not fields[index - 1].isdigit():
210 major = int(match.group(1), 16)
211 minor = int(match.group(2), 16)
212 inode = int(match.group(3))
213 if (major, minor, inode) == (
214 os.major(lock_stat.st_dev),
215 os.minor(lock_stat.st_dev),
218 owners.add(int(fields[index - 1]))
222def _holder_fds(proc_root: Path, pid: int) -> list[Path]:
223 """Return a stable snapshot of the holder's visible file descriptors."""
225 return sorted((proc_root / str(pid) /
"fd").iterdir(), key=
lambda path: path.name)
226 except OSError
as exc:
227 msg =
"cannot enumerate holder file descriptors"
228 raise LockProofError(msg)
from exc
231def _has_locked_inode(fds: list[Path], lock_stat: os.stat_result) -> bool:
232 """Return whether one holder descriptor references the canonical inode."""
235 observed = path.stat()
238 if (observed.st_dev, observed.st_ino) == (lock_stat.st_dev, lock_stat.st_ino):
243def _has_script_digest(fds: list[Path], expected: str) -> bool:
244 """Bind the holder to an open descriptor containing reviewed host bytes."""
245 if not hasattr(os,
"O_PATH"):
249 anchor = os.open(path, os.O_PATH | os.O_CLOEXEC)
251 before = os.fstat(anchor)
252 if not stat.S_ISREG(before.st_mode):
254 descriptor = os.open(
255 Path(
"/proc/self/fd") / str(anchor),
256 os.O_RDONLY | os.O_CLOEXEC | os.O_NONBLOCK,
259 descriptor_before = os.fstat(descriptor)
260 raw = os.read(descriptor, MAX_SCRIPT_BYTES + 1)
261 descriptor_after = os.fstat(descriptor)
264 anchored_after = os.fstat(anchor)
270 (value.st_dev, value.st_ino)
271 for value
in (before, descriptor_before, descriptor_after, anchored_after)
273 stable = len(identities) == 1
and all(
274 stat.S_ISREG(value.st_mode)
275 for value
in (before, descriptor_before, descriptor_after, anchored_after)
277 if stable
and len(raw) <= MAX_SCRIPT_BYTES
and hashlib.sha256(raw).hexdigest() == expected:
282def _process_argv(proc_root: Path, pid: int) -> tuple[Path, list[str]]:
283 """Return one strict executable/argv pair from proc."""
285 executable = (proc_root / str(pid) /
"exe").resolve(strict=
True)
286 argv = (proc_root / str(pid) /
"cmdline").read_bytes().split(b
"\0")
287 except OSError
as exc:
288 msg =
"cannot authenticate holder executable"
289 raise LockProofError(msg)
from exc
290 return executable, [item.decode(
"utf-8",
"strict")
for item
in argv
if item]
296 record: dict[str, object],
297 expected_broker_sha256: str,
299 """Bind the kernel owner to exact broker code and the reviewed Bash parent."""
300 pid = int(record[
"pid"])
301 host_pid = int(record[
"host_pid"])
302 executable, argv = _process_argv(proc_root, pid)
303 expected_python = Path(
"/usr/bin/python3").resolve(strict=
True)
304 if executable != expected_python
or len(argv) != BROKER_ARGC
or argv[1:4] != [
"-I",
"-S",
"-c"]:
305 msg =
"kernel owner is not the fixed isolated broker"
306 raise LockProofError(msg)
307 if hashlib.sha256(argv[4].encode()).hexdigest() != expected_broker_sha256:
308 msg =
"kernel owner is not running the reviewed broker bytes"
309 raise LockProofError(msg)
310 if argv[5:7] != [str(root /
"board.lock"), str(root /
"holder.json")]:
311 msg =
"broker command is not bound to the canonical lock and record"
312 raise LockProofError(msg)
313 if argv[-2:] != [str(host_pid), str(record[
"host_start_ticks"])]:
314 msg =
"broker command is not bound to its recorded Bash parent"
315 raise LockProofError(msg)
316 parent, _ticks = _stat_fields(proc_root, pid)
317 if parent != host_pid:
318 msg =
"broker is not the direct child of the reviewed Bash host"
319 raise LockProofError(msg)
322def _host_shape(proc_root: Path, record: dict[str, object], kind: str) ->
None:
323 """Require the broker parent to be reviewed Bash host bytes without lock fd."""
324 host_pid = int(record[
"host_pid"])
325 executable, argv = _process_argv(proc_root, host_pid)
326 if executable.name !=
"bash" or not any(
327 left ==
"hold" and right == kind
for left, right
in itertools.pairwise(argv)
329 msg =
"broker parent is not the reviewed host hold process"
330 raise LockProofError(msg)
333def _stat_fields(proc_root: Path, pid: int) -> tuple[int, int]:
334 """Return one process parent and start time from proc stat."""
336 raw = (proc_root / str(pid) /
"stat").read_text(encoding=
"ascii")
337 tail = raw[raw.rindex(
")") + 2 :].split()
338 return int(tail[1]), int(tail[19])
339 except (OSError, ValueError, IndexError)
as exc:
340 msg =
"cannot authenticate process identity"
341 raise LockProofError(msg)
from exc
344def _validate_request(request: ProofRequest) ->
None:
345 """Reject malformed or noncanonical proof authorities."""
346 if LOCK_ID_RE.fullmatch(request.lock_id)
is None or request.kind
not in {
"wrapped",
"detached"}:
347 msg =
"requested lock identity is malformed"
348 raise LockProofError(msg)
350 SHA256_RE.fullmatch(digest)
is None
351 for digest
in (request.expected_script_sha256, request.expected_broker_sha256)
353 msg =
"reviewed holder digest is malformed"
354 raise LockProofError(msg)
355 if request.root != CANONICAL_ROOT
and os.environ.get(
"RA8_LOCK_VERIFY_SELFTEST") !=
"1":
356 msg =
"production verification is bound to the canonical lock root"
357 raise LockProofError(msg)
360def _initial_proof(request: ProofRequest) -> _ProofState:
361 """Bind the record, live PIDs, canonical flock, and reviewed source bytes."""
362 record, record_identity = _record(request.root /
"holder.json", request.lock_id, request.kind)
363 pid = int(record[
"pid"])
364 if _start_ticks(request.proc_root, pid) != int(record[
"pid_start_ticks"]):
365 msg =
"holder PID was reused or the record is stale"
366 raise LockProofError(msg)
368 boot = (request.proc_root /
"sys/kernel/random/boot_id").read_text(encoding=
"ascii").strip()
369 lock_stat = (request.root /
"board.lock").stat(follow_symlinks=
False)
370 except OSError
as exc:
371 msg =
"canonical lock or boot identity is unavailable"
372 raise LockProofError(msg)
from exc
373 if stat.S_ISLNK(lock_stat.st_mode)
or not stat.S_ISREG(lock_stat.st_mode):
374 msg =
"canonical board lock is not a regular inode"
375 raise LockProofError(msg)
376 if record[
"boot_id"] != boot
or _lock_owner_pids(request.proc_root, lock_stat) != {pid}:
377 msg =
"record does not match one unambiguous live kernel lock owner"
378 raise LockProofError(msg)
379 broker_fds = _holder_fds(request.proc_root, pid)
380 host_fds = _holder_fds(request.proc_root, int(record[
"host_pid"]))
381 if not _has_locked_inode(broker_fds, lock_stat):
382 msg =
"broker lacks the canonical lock descriptor"
383 raise LockProofError(msg)
384 if _has_script_digest(broker_fds, request.expected_script_sha256):
385 msg =
"broker inherited the reviewed host descriptor"
386 raise LockProofError(msg)
387 if _has_locked_inode(host_fds, lock_stat)
or not _has_script_digest(
388 host_fds, request.expected_script_sha256
390 msg =
"reviewed host is missing or inherited the lock descriptor"
391 raise LockProofError(msg)
392 _broker_shape(request.proc_root, request.root, record, request.expected_broker_sha256)
393 _host_shape(request.proc_root, record, request.kind)
394 return _ProofState(record, record_identity, lock_stat, pid)
397def _final_proof(request: ProofRequest, proof: _ProofState) ->
None:
398 """Recheck every replaceable identity before returning a held verdict."""
399 final_record = (request.root /
"holder.json").stat(follow_symlinks=
False)
400 if (final_record.st_dev, final_record.st_ino) != (
401 proof.record_identity.st_dev,
402 proof.record_identity.st_ino,
404 msg =
"holder record changed before proof completion"
405 raise LockProofError(msg)
406 if _start_ticks(request.proc_root, proof.holder_pid) != int(proof.record[
"pid_start_ticks"]):
407 msg =
"holder exited before proof completion"
408 raise LockProofError(msg)
409 if _start_ticks(request.proc_root, int(proof.record[
"host_pid"])) != int(
410 proof.record[
"host_start_ticks"]
412 msg =
"reviewed host exited before proof completion"
413 raise LockProofError(msg)
414 if _lock_owner_pids(request.proc_root, proof.lock_identity) != {proof.holder_pid}:
415 msg =
"kernel lock changed before proof completion"
416 raise LockProofError(msg)
417 final_lock = (request.root /
"board.lock").stat(follow_symlinks=
False)
418 if (final_lock.st_dev, final_lock.st_ino) != (
419 proof.lock_identity.st_dev,
420 proof.lock_identity.st_ino,
422 msg =
"canonical lock inode changed before proof completion"
423 raise LockProofError(msg)
426def verify(request: ProofRequest) ->
None:
427 """Prove record, process, script, and canonical flock are one live holder."""
428 _validate_request(request)
429 _final_proof(request, _initial_proof(request))
432def _replacement_selftest() -> list[str]:
433 """Prove descriptor reads reject a path replacement after the read."""
434 failures: list[str] = []
435 with tempfile.TemporaryDirectory(prefix=
"ra8-lock-proof-")
as raw:
436 path = Path(raw) /
"holder.json"
437 replacement = path.with_suffix(
".new")
438 path.write_bytes(b
"old")
439 replacement.write_bytes(b
"new")
441 def replace() -> None:
442 replacement.replace(path)
445 _read_regular(path, 16, replace)
446 except LockProofError:
449 failures.append(
"holder record replacement escaped descriptor binding")
453def _stat_line(pid: int, parent: int, ticks: int) -> str:
454 """Render the proc stat fields consumed by this verifier."""
455 return f
"{pid} (holder) S {parent} " +
" ".join([
"0"] * 17 + [str(ticks)]) +
"\n"
458def _write_lock_line(fixture: _Fixture, owners: tuple[int, ...]) ->
None:
459 """Render active FLOCK records for the canonical fixture inode."""
460 observed = fixture.lock.stat()
461 device = f
"{os.major(observed.st_dev):x}:{os.minor(observed.st_dev):x}:{observed.st_ino}"
463 f
"{index}: FLOCK ADVISORY WRITE {pid} {device} 0 EOF" for index, pid
in enumerate(owners, 1)
465 (fixture.proc /
"locks").write_text(
"\n".join(lines) +
"\n", encoding=
"ascii")
468def _write_broker_process(fixture: _Fixture) ->
None:
469 """Create the synthetic isolated broker process and its locked fd."""
475 fixture.broker.read_text(encoding=
"utf-8"),
480 str(fixture.host_pid),
481 str(fixture.host_ticks),
483 process = fixture.proc / str(fixture.pid)
484 (process /
"stat").write_text(
485 _stat_line(fixture.pid, fixture.host_pid, fixture.ticks), encoding=
"ascii"
487 (process /
"cmdline").write_bytes(b
"\0".join(item.encode()
for item
in argv) + b
"\0")
488 (process /
"exe").symlink_to(
"/usr/bin/python3")
489 (process /
"fd/3").symlink_to(fixture.lock)
492def _write_host_process(proc: Path, script: Path, pid: int, ticks: int) ->
None:
493 """Create the reviewed Bash parent without a lock descriptor."""
494 (proc / str(pid) /
"stat").write_text(_stat_line(pid, 12, ticks), encoding=
"ascii")
495 (proc / str(pid) /
"cmdline").write_bytes(b
"bash\0bench_host.sh\0hold\0wrapped\0")
496 (proc / str(pid) /
"exe").symlink_to(
"/bin/bash")
497 (proc / str(pid) /
"fd/4").symlink_to(script)
500def _make_fixture(base: Path) -> _Fixture:
501 """Create a complete synthetic Linux proc and canonical lock authority."""
502 root = base /
"bench"
508 lock_id =
"0123456789abcdef"
509 (proc / str(pid) /
"fd").mkdir(parents=
True)
510 (proc / str(host_pid) /
"fd").mkdir(parents=
True)
511 (proc /
"sys/kernel/random").mkdir(parents=
True)
513 lock = root /
"board.lock"
514 script = root /
"bench_host.sh"
515 broker = root /
"bench_lock_broker.py"
516 record = root /
"holder.json"
517 lock.write_bytes(b
"")
518 script.write_bytes(b
"#!/bin/bash\n")
519 broker.write_bytes(b
"# broker fixture\n")
520 boot_id =
"12345678-1234-1234-1234-123456789abc"
521 (proc /
"sys/kernel/random/boot_id").write_text(boot_id +
"\n", encoding=
"ascii")
527 "hold_kind":
"wrapped",
529 "pid_start_ticks": ticks,
530 "host_pid": host_pid,
531 "host_start_ticks": host_ticks,
550 _write_broker_process(fixture)
551 _write_host_process(proc, script, host_pid, host_ticks)
552 _write_lock_line(fixture, (pid,))
556def _expect_refusal(fixture: _Fixture, label: str, failures: list[str]) ->
None:
557 """Require the current fixture state to fail authentication."""
559 verify(fixture.request)
560 except LockProofError:
562 failures.append(f
"{label} escaped live lock authentication")
565def _authority_selftest(fixture: _Fixture) -> list[str]:
566 """Attack kernel ownership, record identity, PID reuse, and script binding."""
567 failures: list[str] = []
568 verify(fixture.request)
569 _write_lock_line(fixture, ())
570 _expect_refusal(fixture,
"forged record without flock", failures)
571 _write_lock_line(fixture, (fixture.pid + 1,))
572 _expect_refusal(fixture,
"unrelated flock", failures)
573 _write_lock_line(fixture, (fixture.pid, fixture.pid + 1))
574 _expect_refusal(fixture,
"ambiguous flock ownership", failures)
575 _write_lock_line(fixture, (fixture.pid,))
576 stat_path = fixture.proc / str(fixture.pid) /
"stat"
577 stat_path.write_text(_stat_line(fixture.pid, 12, fixture.ticks + 1), encoding=
"ascii")
578 _expect_refusal(fixture,
"reused holder PID", failures)
579 stat_path.write_text(_stat_line(fixture.pid, 12, fixture.ticks), encoding=
"ascii")
581 verify(replace(fixture.request, expected_script_sha256=
"f" * 64))
582 except LockProofError:
585 failures.append(
"tampered reviewed-holder digest was accepted")
587 verify(replace(fixture.request, expected_broker_sha256=
"e" * 64))
588 except LockProofError:
591 failures.append(
"tampered reviewed-broker digest was accepted")
592 leaked = fixture.proc / str(fixture.host_pid) /
"fd/9"
593 leaked.symlink_to(fixture.lock)
594 _expect_refusal(fixture,
"lock descriptor leaked to Bash host", failures)
596 broker_leak = fixture.proc / str(fixture.pid) /
"fd/4"
597 broker_leak.symlink_to(fixture.script)
598 _expect_refusal(fixture,
"reviewed host descriptor leaked to broker", failures)
603def _race_selftest(fixture: _Fixture) -> list[str]:
604 """Attack record/lock replacement and holder exit between proof phases."""
605 failures: list[str] = []
606 real_ticks = _start_ticks
608 def replace_record(proc_root: Path, pid: int) -> int:
609 if fixture.record.exists():
610 replacement = fixture.record.with_suffix(
".replacement")
611 replacement.write_bytes(fixture.record.read_bytes())
612 replacement.replace(fixture.record)
613 return real_ticks(proc_root, pid)
615 with mock.patch.object(sys.modules[__name__],
"_start_ticks", side_effect=replace_record):
616 _expect_refusal(fixture,
"holder record inode replacement", failures)
618 fixture.record.write_bytes(fixture.record.read_bytes())
619 with mock.patch.object(
620 sys.modules[__name__],
622 side_effect=[fixture.ticks, LockProofError(
"holder exited")],
624 _expect_refusal(fixture,
"holder exit during proof", failures)
625 real_owners = _lock_owner_pids
628 def replace_lock(proc_root: Path, observed: os.stat_result) -> set[int]:
631 result = real_owners(proc_root, observed)
632 if calls == SECOND_LOCK_OBSERVATION:
633 replacement = fixture.lock.with_suffix(
".replacement")
634 replacement.write_bytes(b
"")
635 replacement.replace(fixture.lock)
638 with mock.patch.object(sys.modules[__name__],
"_lock_owner_pids", side_effect=replace_lock):
639 _expect_refusal(fixture,
"canonical lock inode replacement", failures)
643def _live_fields(lock_id: str) -> str:
644 """Return one valid wrapped request for the real-process descriptor test."""
648 "holder_class":
"agent",
649 "holder_name":
"selftest",
650 "intent":
"offline live descriptor selftest",
652 "hold_kind":
"wrapped",
653 "break_glass":
"false",
654 "origin":
"selftest",
657 raw =
"".join(f
"{key}={value}\n" for key, value
in fields.items()).encode()
658 return base64.b64encode(raw).decode(
"ascii")
661def _start_live_host(base: Path, lock_id: str) -> tuple[subprocess.Popen[str], ProofRequest]:
662 """Start the actual Bash host and Python broker against a throwaway lock."""
663 source_dir = Path(__file__).resolve().parent
664 host_source = (source_dir /
"bench_host.sh").read_bytes()
665 broker_source = (source_dir /
"bench_lock_broker.py").read_bytes()
666 host = base /
"bench_host.sh"
667 broker = base /
"bench_lock_broker.py"
668 root = base /
"bench"
669 host.write_bytes(host_source)
670 broker.write_bytes(broker_source)
672 "PATH":
"/usr/bin:/bin",
673 "RA8_BENCH_DIR": str(root),
674 "RA8_BENCH_BROKER_SRC": str(broker),
676 process = subprocess.Popen(
677 [
"/bin/bash", str(host),
"hold",
"wrapped",
"0", _live_fields(lock_id)],
679 stdin=subprocess.PIPE,
680 stdout=subprocess.PIPE,
681 stderr=subprocess.PIPE,
684 request = ProofRequest(
687 hashlib.sha256(host_source).hexdigest(),
688 hashlib.sha256(broker_source).hexdigest(),
691 return process, request
694def _proof_timeout(_signum: int, _frame: object) ->
None:
695 """Interrupt a verifier that reads the wrapped liveness pipe."""
696 message =
"live descriptor proof blocked"
697 raise _ProofDeadlineError(message)
700def _verify_bounded(request: ProofRequest) ->
None:
701 """Run one real-process proof with a hard nonblocking deadline."""
702 previous_handler = signal.signal(signal.SIGALRM, _proof_timeout)
703 previous_timer = signal.setitimer(signal.ITIMER_REAL, 5.0)
707 signal.setitimer(signal.ITIMER_REAL, *previous_timer)
708 signal.signal(signal.SIGALRM, previous_handler)
711def _stop_live_host(process: subprocess.Popen[str]) ->
None:
712 """Close the liveness pipe and reap only this throwaway holder."""
713 if process.stdin
is not None:
714 process.stdin.close()
716 process.wait(timeout=5)
717 except subprocess.TimeoutExpired:
719 process.wait(timeout=5)
722def _digest_scan_bounded(fds: list[Path], expected: str) -> bool:
723 """Run one descriptor digest scan with a hard liveness deadline."""
724 previous_handler = signal.signal(signal.SIGALRM, _proof_timeout)
725 previous_timer = signal.setitimer(signal.ITIMER_REAL, 2.0)
727 return _has_script_digest(fds, expected)
729 signal.setitimer(signal.ITIMER_REAL, *previous_timer)
730 signal.signal(signal.SIGALRM, previous_handler)
733def _prefilled_pipe_scan(script: Path, expected: str, payload: bytes) -> tuple[bool, bytes]:
734 """Scan a pipe before a regular file and return its unconsumed payload."""
735 read_fd, write_fd = os.pipe()
737 os.write(write_fd, payload)
738 matched = _has_script_digest([Path(f
"/proc/self/fd/{read_fd}"), script], expected)
739 os.set_blocking(read_fd,
False)
741 unread = os.read(read_fd, len(payload))
742 except BlockingIOError:
747 return matched, unread
750def _pipe_descriptor_selftest() -> list[str]:
751 """Prove descriptor classification never consumes a pipe before a file."""
752 failures: list[str] = []
753 with tempfile.TemporaryDirectory(prefix=
"ra8-lock-pipe-fd-")
as raw:
755 script = root /
"bench_host.sh"
756 script.write_bytes(b
"#!/bin/bash\n# reviewed host\n")
757 expected = hashlib.sha256(script.read_bytes()).hexdigest()
758 payload = b
"pipe bytes are not reviewed host source"
759 pipe_digest = hashlib.sha256(payload).hexdigest()
760 false_match, false_unread = _prefilled_pipe_scan(script, pipe_digest, payload)
761 if false_match
or false_unread != payload:
762 failures.append(
"non-regular fd supplied or consumed a matching digest")
763 true_match, true_unread = _prefilled_pipe_scan(script, expected, payload)
764 if not true_match
or true_unread != payload:
765 failures.append(
"non-regular fd was consumed before the reviewed regular fd")
766 fifo = root /
"blocking.fifo"
769 fifo_match = _digest_scan_bounded([fifo, script], expected)
770 except _ProofDeadlineError:
771 failures.append(
"opening a non-regular fd blocked before the reviewed regular fd")
774 failures.append(
"a skipped FIFO prevented the reviewed regular fd from matching")
778def _live_digest_controls(request: ProofRequest, host_fds: list[Path], base: Path) -> list[str]:
779 """Attack a real holder's digest and its deleted script pathname."""
780 failures: list[str] = []
782 _verify_bounded(replace(request, expected_script_sha256=
"f" * 64))
783 except LockProofError:
786 failures.append(
"real Bash host accepted the wrong reviewed script digest")
787 replacement_payload = b
"#!/bin/bash\n# path replacement\n"
788 replacement_path = base /
"bench_host.sh"
789 replacement_path.write_bytes(replacement_payload)
791 _verify_bounded(request)
792 except (LockProofError, OSError, _ProofDeadlineError)
as exc:
793 failures.append(f
"script path replacement displaced the open host inode: {exc}")
794 replacement_digest = hashlib.sha256(replacement_payload).hexdigest()
795 if _has_script_digest(host_fds, replacement_digest):
796 failures.append(
"script path replacement redirected the open host descriptor")
800def _live_descriptor_selftest() -> list[str]:
801 """Prove the verifier never reads the real host's liveness pipe."""
802 if not Path(
"/proc/self/fd").is_dir():
804 failures: list[str] = []
805 with tempfile.TemporaryDirectory(prefix=
"ra8-lock-live-")
as raw:
806 process, request = _start_live_host(Path(raw),
"fedcba9876543210")
808 if process.stdout
is None or not select.select([process.stdout], [], [], 5)[0]:
809 return [
"real Bash host did not acknowledge its throwaway lock"]
810 if process.stdout.readline() !=
"bench: ACQUIRED fedcba9876543210\n":
811 return [
"real Bash host returned the wrong acquisition identity"]
813 _verify_bounded(request)
814 except (LockProofError, OSError, _ProofDeadlineError)
as exc:
815 failures.append(f
"real Bash host proof failed: {exc}")
817 record, _identity = _record(request.root /
"holder.json", request.lock_id, request.kind)
818 lock_stat = (request.root /
"board.lock").stat(follow_symlinks=
False)
819 broker_fds = _holder_fds(request.proc_root, int(record[
"pid"]))
820 host_fds = _holder_fds(request.proc_root, int(record[
"host_pid"]))
821 if not _has_locked_inode(broker_fds, lock_stat):
822 failures.append(
"real broker lost its lock descriptor")
823 if _has_locked_inode(host_fds, lock_stat):
824 failures.append(
"real Bash host inherited the lock descriptor")
825 if not _has_script_digest(host_fds, request.expected_script_sha256):
826 failures.append(
"real Bash host lost the reviewed script descriptor")
827 if _has_script_digest(broker_fds, request.expected_script_sha256):
828 failures.append(
"real broker inherited the reviewed script descriptor")
829 failures.extend(_live_digest_controls(request, host_fds, Path(raw)))
831 _stop_live_host(process)
835def run_selftest() -> list[str]:
836 """Run deterministic offline attacks against every lock proof boundary."""
837 previous = os.environ.get(
"RA8_LOCK_VERIFY_SELFTEST")
838 os.environ[
"RA8_LOCK_VERIFY_SELFTEST"] =
"1"
840 with tempfile.TemporaryDirectory(prefix=
"ra8-lock-authority-")
as raw:
841 fixture = _make_fixture(Path(raw))
843 _replacement_selftest()
844 + _authority_selftest(fixture)
845 + _race_selftest(fixture)
846 + _pipe_descriptor_selftest()
847 + _live_descriptor_selftest()
851 os.environ.pop(
"RA8_LOCK_VERIFY_SELFTEST",
None)
853 os.environ[
"RA8_LOCK_VERIFY_SELFTEST"] = previous
857 """Parse one fixed verification request and report a fail-closed verdict."""
858 if sys.argv[1:] == [
"--selftest"]:
859 failures = run_selftest()
860 for failure
in failures:
861 print(f
"bench-lock-verify selftest: {failure}", file=sys.stderr)
862 return 1
if failures
else 0
863 parser = argparse.ArgumentParser(description=__doc__)
864 parser.add_argument(
"lock_id")
865 parser.add_argument(
"hold_kind", choices=(
"wrapped",
"detached"))
866 parser.add_argument(
"expected_script_sha256")
867 parser.add_argument(
"expected_broker_sha256")
868 args = parser.parse_args()
874 args.expected_script_sha256,
875 args.expected_broker_sha256,
878 except (LockProofError, OSError, UnicodeError)
as exc:
879 print(f
"bench-lock-verify: {exc}", file=sys.stderr)
881 print(
"bench-lock-verify: HELD")
885if __name__ ==
"__main__":
886 raise SystemExit(
main())
void main(void)
The application entry point Reset_Handler hands control to.