240 lines
9.5 KiB
Python
240 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
"""Host-only transactional mapping/protection model for admitted ELF loads."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from phase10ag_bounded_elf import LoadSegment, PF_R, PF_W, PF_X
|
|
|
|
|
|
PAGE_SIZE = 16 * 1024
|
|
MAX_SEGMENTS = 8
|
|
MAX_RELATIVE_RELOCATIONS = 4096
|
|
MAX_EVENTS = 64
|
|
MAX_MAIN_TICKS = 128
|
|
MAX_REGION_SIZE = 128 * 1024 * 1024
|
|
|
|
RESERVE_CHILD = "RESERVE_CHILD"
|
|
CREATE_MIRROR = "CREATE_MIRROR"
|
|
COPY_FILE_BYTES = "COPY_FILE_BYTES"
|
|
ZERO_BSS = "ZERO_BSS"
|
|
APPLY_RELATIVE = "APPLY_RELATIVE"
|
|
COPY_MIRROR_TO_CHILD = "COPY_MIRROR_TO_CHILD"
|
|
SET_FINAL_PROTECTION = "SET_FINAL_PROTECTION"
|
|
SYNC_IMAGE = "SYNC_IMAGE"
|
|
RELEASE_MIRROR = "RELEASE_MIRROR"
|
|
UNMAP_CHILD = "UNMAP_CHILD"
|
|
OK = "OK"
|
|
ERROR = "ERROR"
|
|
|
|
OPERATIONS = {
|
|
RESERVE_CHILD, CREATE_MIRROR, COPY_FILE_BYTES, ZERO_BSS,
|
|
APPLY_RELATIVE, COPY_MIRROR_TO_CHILD, SET_FINAL_PROTECTION,
|
|
SYNC_IMAGE, RELEASE_MIRROR, UNMAP_CHILD,
|
|
}
|
|
|
|
|
|
class MappingModelError(RuntimeError):
|
|
"""The mapping transaction or its rollback is incomplete."""
|
|
|
|
|
|
def _round_down(value: int) -> int:
|
|
return value & ~(PAGE_SIZE - 1)
|
|
|
|
|
|
def _round_up(value: int) -> int:
|
|
if value > (1 << 64) - PAGE_SIZE:
|
|
raise MappingModelError("mapping address cannot be rounded")
|
|
return (value + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MappingPlan:
|
|
segments: tuple[LoadSegment, ...]
|
|
relative_relocations: int
|
|
|
|
def __post_init__(self) -> None:
|
|
if not isinstance(self.segments, tuple) or not 1 <= len(self.segments) <= MAX_SEGMENTS \
|
|
or any(type(item) is not LoadSegment for item in self.segments):
|
|
raise MappingModelError("mapping segments are invalid")
|
|
if tuple(sorted(self.segments, key=lambda item: item.virtual_address)) != self.segments:
|
|
raise MappingModelError("mapping segments are not ordered")
|
|
if not isinstance(self.relative_relocations, int) \
|
|
or isinstance(self.relative_relocations, bool) \
|
|
or not 1 <= self.relative_relocations <= MAX_RELATIVE_RELOCATIONS:
|
|
raise MappingModelError("relative relocation count is invalid")
|
|
protected: list[tuple[int, int]] = []
|
|
for segment in self.segments:
|
|
if segment.memory_size <= 0 or segment.file_size < 0 \
|
|
or segment.file_size > segment.memory_size \
|
|
or segment.flags & ~(PF_R | PF_W | PF_X) \
|
|
or segment.flags & PF_W and segment.flags & PF_X:
|
|
raise MappingModelError("mapping segment is unsafe")
|
|
start = _round_down(segment.virtual_address)
|
|
end = _round_up(segment.virtual_address + segment.memory_size)
|
|
if protected and protected[-1][1] > start:
|
|
raise MappingModelError("page-rounded protections overlap")
|
|
protected.append((start, end))
|
|
if self.region_size > MAX_REGION_SIZE:
|
|
raise MappingModelError("mapping region exceeds budget")
|
|
|
|
@property
|
|
def region_start(self) -> int:
|
|
return _round_down(self.segments[0].virtual_address)
|
|
|
|
@property
|
|
def region_size(self) -> int:
|
|
end = _round_up(self.segments[-1].virtual_address +
|
|
self.segments[-1].memory_size)
|
|
return end - self.region_start
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FakeMappingEvent:
|
|
operation: str
|
|
result: str
|
|
segment_index: int = -1
|
|
value: int = 0
|
|
ticks: int = 1
|
|
|
|
def __post_init__(self) -> None:
|
|
if self.operation not in OPERATIONS or self.result not in {OK, ERROR}:
|
|
raise MappingModelError("fake mapping operation/result is invalid")
|
|
if not isinstance(self.segment_index, int) or isinstance(self.segment_index, bool) \
|
|
or self.segment_index < -1:
|
|
raise MappingModelError("fake segment index is invalid")
|
|
if not isinstance(self.value, int) or isinstance(self.value, bool) or self.value < 0:
|
|
raise MappingModelError("fake mapping value is invalid")
|
|
if not isinstance(self.ticks, int) or isinstance(self.ticks, bool) \
|
|
or not 1 <= self.ticks <= MAX_MAIN_TICKS:
|
|
raise MappingModelError("fake mapping ticks are invalid")
|
|
indexed = self.operation in {COPY_FILE_BYTES, ZERO_BSS, SET_FINAL_PROTECTION}
|
|
if indexed != (self.segment_index >= 0):
|
|
raise MappingModelError("fake mapping segment binding is invalid")
|
|
|
|
|
|
class FakeMappingFacade:
|
|
"""Exact scripted facade with no memory or process capability."""
|
|
|
|
def __init__(self, events: tuple[FakeMappingEvent, ...]) -> None:
|
|
if not isinstance(events, tuple) or not 1 <= len(events) <= MAX_EVENTS \
|
|
or any(type(event) is not FakeMappingEvent for event in events):
|
|
raise MappingModelError("fake mapping script is invalid")
|
|
self._events = list(events)
|
|
self.trace: list[str] = []
|
|
self.ticks = 0
|
|
|
|
def invoke(self, operation: str, segment_index: int = -1,
|
|
value: int = 0, cleanup: bool = False) -> FakeMappingEvent:
|
|
if not self._events:
|
|
raise MappingModelError("fake mapping script is exhausted")
|
|
event = self._events.pop(0)
|
|
if event.operation != operation or event.segment_index != segment_index \
|
|
or event.value != value:
|
|
raise MappingModelError("fake mapping ordering or binding differs")
|
|
if not cleanup and self.ticks + event.ticks > MAX_MAIN_TICKS:
|
|
self.trace.append(f"DEADLINE_BEFORE:{operation}")
|
|
raise MappingModelError("mapping main tick budget expired before operation")
|
|
self.ticks += event.ticks
|
|
self.trace.append(f"{operation}:{segment_index}:{value}:{event.result}")
|
|
return event
|
|
|
|
@property
|
|
def remaining(self) -> int:
|
|
return len(self._events)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MappingOutcome:
|
|
classification: str
|
|
success: bool
|
|
region_size: int
|
|
copied_file_bytes: int
|
|
zeroed_bss_bytes: int
|
|
relative_relocations: int
|
|
final_protections: tuple[int, ...]
|
|
mirror_released: bool
|
|
child_region_retained: bool
|
|
child_region_unmapped: bool
|
|
target_mapping_performed: bool = False
|
|
device_behavior_proven: bool = False
|
|
|
|
|
|
def _ok(facade: FakeMappingFacade, operation: str, segment_index: int = -1,
|
|
value: int = 0) -> None:
|
|
if facade.invoke(operation, segment_index, value).result != OK:
|
|
raise MappingModelError(f"{operation} failed")
|
|
|
|
|
|
def run_mapping(plan: MappingPlan, facade: FakeMappingFacade) -> MappingOutcome:
|
|
"""Run one synthetic mapping transaction with full-region rollback."""
|
|
if type(plan) is not MappingPlan or type(facade) is not FakeMappingFacade:
|
|
raise MappingModelError("mapping model boundary is invalid")
|
|
child_reserved = False
|
|
mirror_active = False
|
|
mirror_released = False
|
|
child_unmapped = False
|
|
committed = False
|
|
copied = 0
|
|
zeroed = 0
|
|
protections: list[int] = []
|
|
primary_error: Exception | None = None
|
|
|
|
try:
|
|
_ok(facade, RESERVE_CHILD, value=plan.region_size)
|
|
child_reserved = True
|
|
_ok(facade, CREATE_MIRROR, value=plan.region_size)
|
|
mirror_active = True
|
|
for index, segment in enumerate(plan.segments):
|
|
if segment.file_size:
|
|
_ok(facade, COPY_FILE_BYTES, index, segment.file_size)
|
|
copied += segment.file_size
|
|
bss = segment.memory_size - segment.file_size
|
|
if bss:
|
|
_ok(facade, ZERO_BSS, index, bss)
|
|
zeroed += bss
|
|
_ok(facade, APPLY_RELATIVE, value=plan.relative_relocations)
|
|
_ok(facade, COPY_MIRROR_TO_CHILD, value=plan.region_size)
|
|
for index, segment in enumerate(plan.segments):
|
|
_ok(facade, SET_FINAL_PROTECTION, index, segment.flags)
|
|
protections.append(segment.flags)
|
|
_ok(facade, SYNC_IMAGE, value=plan.region_size)
|
|
_ok(facade, RELEASE_MIRROR)
|
|
mirror_active = False
|
|
mirror_released = True
|
|
committed = True
|
|
except Exception as error:
|
|
primary_error = error
|
|
finally:
|
|
try:
|
|
if mirror_active:
|
|
if facade.invoke(RELEASE_MIRROR, cleanup=True).result != OK:
|
|
raise MappingModelError("mirror cleanup failed")
|
|
mirror_active = False
|
|
mirror_released = True
|
|
if child_reserved and not committed:
|
|
if facade.invoke(UNMAP_CHILD, value=plan.region_size,
|
|
cleanup=True).result != OK:
|
|
raise MappingModelError("child mapping rollback failed")
|
|
child_unmapped = True
|
|
child_reserved = False
|
|
except Exception as cleanup_error:
|
|
raise MappingModelError("mapping rollback is incomplete") from cleanup_error
|
|
|
|
if facade.remaining:
|
|
raise MappingModelError("fake mapping script has unused operations")
|
|
if primary_error is not None:
|
|
return MappingOutcome(
|
|
"OFFLINE_MAPPING_TRANSACTION_ROLLED_BACK", False, plan.region_size,
|
|
copied, zeroed, plan.relative_relocations, tuple(protections),
|
|
mirror_released, False, child_unmapped)
|
|
if not committed or not mirror_released or child_unmapped \
|
|
or tuple(protections) != tuple(item.flags for item in plan.segments):
|
|
raise MappingModelError("successful mapping invariants failed")
|
|
return MappingOutcome(
|
|
"OFFLINE_MAPPING_TRANSACTION_MODEL_COMPLETE", True, plan.region_size,
|
|
copied, zeroed, plan.relative_relocations, tuple(protections), True,
|
|
True, False)
|