198 lines
7.0 KiB
Python
198 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Capability-free exact-progress copy and credential restoration model."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
MAX_COPY_SIZE = 128 * 1024 * 1024
|
|
MAX_CHUNKS = 4096
|
|
MAX_EVENTS = MAX_CHUNKS + 16
|
|
MAX_TICKS = 8192
|
|
MAX_U64 = (1 << 64) - 1
|
|
|
|
BACKUP_AUTHID = "BACKUP_AUTHID"
|
|
BACKUP_CAPS = "BACKUP_CAPS"
|
|
SET_PRIV_AUTHID = "SET_PRIV_AUTHID"
|
|
SET_PRIV_CAPS = "SET_PRIV_CAPS"
|
|
COPY_CHUNK = "COPY_CHUNK"
|
|
RESTORE_CAPS = "RESTORE_CAPS"
|
|
RESTORE_AUTHID = "RESTORE_AUTHID"
|
|
KILL_AND_REAP_CHILD = "KILL_AND_REAP_CHILD"
|
|
TERMINATE_SERVICE = "TERMINATE_SERVICE"
|
|
OK = "OK"
|
|
ERROR = "ERROR"
|
|
MORE = "MORE"
|
|
COMPLETE = "COMPLETE"
|
|
|
|
OPERATIONS = {
|
|
BACKUP_AUTHID, BACKUP_CAPS, SET_PRIV_AUTHID, SET_PRIV_CAPS, COPY_CHUNK,
|
|
RESTORE_CAPS, RESTORE_AUTHID, KILL_AND_REAP_CHILD, TERMINATE_SERVICE,
|
|
}
|
|
|
|
|
|
class CopyModelError(RuntimeError):
|
|
"""The synthetic copy contract cannot reach a bounded safe state."""
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CopyPlan:
|
|
source: int
|
|
destination: int
|
|
length: int
|
|
|
|
def __post_init__(self) -> None:
|
|
for value in (self.source, self.destination, self.length):
|
|
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
|
|
raise CopyModelError("copy plan value is invalid")
|
|
if not 1 <= self.length <= MAX_COPY_SIZE:
|
|
raise CopyModelError("copy length is invalid")
|
|
if self.source > MAX_U64 - self.length \
|
|
or self.destination > MAX_U64 - self.length:
|
|
raise CopyModelError("copy range overflows")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FakeCopyEvent:
|
|
operation: str
|
|
result: str
|
|
progress: int = 0
|
|
remote_status: str = ""
|
|
ticks: int = 1
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.operation not in OPERATIONS or self.result not in {OK, ERROR}:
|
|
raise CopyModelError("copy event operation/result is invalid")
|
|
if not isinstance(self.progress, int) or isinstance(self.progress, bool) \
|
|
or self.progress < 0:
|
|
raise CopyModelError("copy progress is invalid")
|
|
if self.operation == COPY_CHUNK:
|
|
if self.remote_status not in {MORE, COMPLETE}:
|
|
raise CopyModelError("copy status is invalid")
|
|
elif self.progress or self.remote_status:
|
|
raise CopyModelError("non-copy event carries copy result")
|
|
if not isinstance(self.ticks, int) or isinstance(self.ticks, bool) \
|
|
or not 1 <= self.ticks <= MAX_TICKS:
|
|
raise CopyModelError("copy event ticks are invalid")
|
|
|
|
|
|
class FakeCopyFacade:
|
|
"""Exact fake script with no credential, process, clock or memory access."""
|
|
|
|
def __init__(self, events: tuple[FakeCopyEvent, ...]) -> None:
|
|
if not isinstance(events, tuple) or not 1 <= len(events) <= MAX_EVENTS \
|
|
or any(type(item) is not FakeCopyEvent for item in events):
|
|
raise CopyModelError("copy event script is invalid")
|
|
self._events = list(events)
|
|
self.ticks = 0
|
|
|
|
def invoke(self, operation: str, cleanup: bool = False) -> FakeCopyEvent:
|
|
if not self._events:
|
|
raise CopyModelError("copy event script is exhausted")
|
|
event = self._events.pop(0)
|
|
if event.operation != operation:
|
|
raise CopyModelError("copy event ordering differs")
|
|
if not cleanup and self.ticks + event.ticks > MAX_TICKS:
|
|
raise CopyModelError("copy deadline precedes operation")
|
|
self.ticks += event.ticks
|
|
return event
|
|
|
|
@property
|
|
def remaining(self) -> int:
|
|
return len(self._events)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CopyOutcome:
|
|
classification: str
|
|
success: bool
|
|
copied: int
|
|
restore_failure_bits: int
|
|
child_alive: bool
|
|
service_available: bool
|
|
target_copy_performed: bool = False
|
|
firmware_behavior_proven: bool = False
|
|
|
|
|
|
def run_copy(plan: CopyPlan, facade: FakeCopyFacade) -> CopyOutcome:
|
|
"""Run one bounded synthetic copy and attempt every required restore."""
|
|
if type(plan) is not CopyPlan or type(facade) is not FakeCopyFacade:
|
|
raise CopyModelError("copy model boundary is invalid")
|
|
auth_changed = caps_changed = False
|
|
copied = chunks = restore_failures = 0
|
|
failed = False
|
|
|
|
try:
|
|
if facade.invoke(BACKUP_AUTHID).result != OK:
|
|
failed = True
|
|
elif facade.invoke(BACKUP_CAPS).result != OK:
|
|
failed = True
|
|
elif facade.invoke(SET_PRIV_AUTHID).result != OK:
|
|
failed = True
|
|
else:
|
|
auth_changed = True
|
|
if facade.invoke(SET_PRIV_CAPS).result != OK:
|
|
failed = True
|
|
else:
|
|
caps_changed = True
|
|
while copied < plan.length:
|
|
if chunks >= MAX_CHUNKS:
|
|
failed = True
|
|
break
|
|
event = facade.invoke(COPY_CHUNK)
|
|
chunks += 1
|
|
remaining = plan.length - copied
|
|
if event.result != OK or event.progress == 0 \
|
|
or event.progress > remaining:
|
|
failed = True
|
|
break
|
|
copied += event.progress
|
|
if event.remote_status == COMPLETE:
|
|
if copied != plan.length:
|
|
failed = True
|
|
break
|
|
if copied == plan.length:
|
|
failed = True
|
|
break
|
|
if copied != plan.length:
|
|
failed = True
|
|
except CopyModelError:
|
|
failed = True
|
|
|
|
if caps_changed:
|
|
if facade.invoke(RESTORE_CAPS, cleanup=True).result != OK:
|
|
restore_failures |= 1
|
|
caps_changed = False
|
|
if auth_changed:
|
|
if facade.invoke(RESTORE_AUTHID, cleanup=True).result != OK:
|
|
restore_failures |= 2
|
|
auth_changed = False
|
|
|
|
child_alive = True
|
|
service_available = True
|
|
if (failed and copied) or restore_failures:
|
|
if facade.invoke(KILL_AND_REAP_CHILD, cleanup=True).result != OK:
|
|
raise CopyModelError("child termination failed")
|
|
child_alive = False
|
|
if restore_failures:
|
|
if facade.invoke(TERMINATE_SERVICE, cleanup=True).result != OK:
|
|
raise CopyModelError("compromised service termination failed")
|
|
service_available = False
|
|
if facade.remaining:
|
|
raise CopyModelError("copy event script has unused operations")
|
|
|
|
success = not failed and copied == plan.length and restore_failures == 0
|
|
if success:
|
|
return CopyOutcome("OFFLINE_EXACT_COPY_COMPLETE", True, copied, 0,
|
|
True, True)
|
|
if restore_failures:
|
|
classification = "OFFLINE_RESTORE_FAILURE_CONTAINED"
|
|
elif copied:
|
|
classification = "OFFLINE_PARTIAL_COPY_CONTAINED"
|
|
else:
|
|
classification = "OFFLINE_COPY_REJECTED_BEFORE_MUTATION"
|
|
return CopyOutcome(classification, False, copied, restore_failures,
|
|
child_alive, service_available)
|