Files
chimera-gfx-Public/tools/simulate_phase09_transaction.py
Chimera GFX release export a6037502d7
phase0-ci / build-and-audit (push) Successful in 2m14s
Publish Chimera GFX source
2026-09-03 03:27:14 +02:00

1009 lines
31 KiB
Python

#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Host-only virtual simulator for the Phase-0.9A anti-brick design.
The model never opens a target artifact, touches a device path, starts a
compiler, or performs network I/O. All objects and writes are logical records
held in memory. A successful host-model result is not PS5 hardware evidence.
"""
from __future__ import annotations
import argparse
import copy
import json
from dataclasses import asdict, dataclass
from typing import Any, Iterable
STATES = (
"OFFLINE_ONLY",
"OBSERVATION_NOT_AUTHORIZED",
"OBSERVATION_AUTHORIZED",
"DEVICE_IDENTITY_OBSERVED",
"LIVE_OBJECTS_VERIFIED",
"BACKUP_NOT_PRESENT",
"BACKUP_CREATION_NOT_AUTHORIZED",
"BACKUP_CREATION_AUTHORIZED",
"BACKUP_CREATED",
"BACKUP_REOPENED_AND_VERIFIED",
"RECOVERY_PATH_VERIFIED",
"CANDIDATE_NOT_AUTHORIZED",
"CANDIDATE_APPROVED_OFFLINE",
"STAGING_NOT_AUTHORIZED",
"STAGING_AUTHORIZED",
"CANDIDATE_STAGED",
"CANDIDATE_REOPENED_AND_VERIFIED",
"TARGET_NOT_QUIESCENT",
"TARGET_QUIESCENT",
"SWITCH_NOT_AUTHORIZED",
"SWITCH_AUTHORIZED",
"SWITCH_IN_PROGRESS",
"POST_SWITCH_VERIFY",
"MANUAL_EXECUTION_NOT_AUTHORIZED",
"MANUAL_EXECUTION_AUTHORIZED",
"ONE_SHOT_EXECUTION",
"ACCEPTED",
"ROLLBACK_REQUIRED",
"ROLLBACK_AUTHORIZED",
"ROLLBACK_IN_PROGRESS",
"ROLLBACK_VERIFIED",
"BLOCKED",
)
ALLOWED_FORWARD_TRANSITIONS = {
("OFFLINE_ONLY", "OBSERVATION_NOT_AUTHORIZED"),
("OBSERVATION_NOT_AUTHORIZED", "OBSERVATION_AUTHORIZED"),
("OBSERVATION_AUTHORIZED", "DEVICE_IDENTITY_OBSERVED"),
("DEVICE_IDENTITY_OBSERVED", "LIVE_OBJECTS_VERIFIED"),
("LIVE_OBJECTS_VERIFIED", "BACKUP_NOT_PRESENT"),
("BACKUP_NOT_PRESENT", "BACKUP_CREATION_NOT_AUTHORIZED"),
("BACKUP_CREATION_NOT_AUTHORIZED", "BACKUP_CREATION_AUTHORIZED"),
("BACKUP_CREATION_AUTHORIZED", "BACKUP_CREATED"),
("BACKUP_CREATED", "BACKUP_REOPENED_AND_VERIFIED"),
("BACKUP_REOPENED_AND_VERIFIED", "RECOVERY_PATH_VERIFIED"),
("RECOVERY_PATH_VERIFIED", "CANDIDATE_NOT_AUTHORIZED"),
("CANDIDATE_NOT_AUTHORIZED", "CANDIDATE_APPROVED_OFFLINE"),
("CANDIDATE_APPROVED_OFFLINE", "STAGING_NOT_AUTHORIZED"),
("STAGING_NOT_AUTHORIZED", "STAGING_AUTHORIZED"),
("STAGING_AUTHORIZED", "CANDIDATE_STAGED"),
("CANDIDATE_STAGED", "CANDIDATE_REOPENED_AND_VERIFIED"),
("CANDIDATE_REOPENED_AND_VERIFIED", "TARGET_NOT_QUIESCENT"),
("TARGET_NOT_QUIESCENT", "TARGET_QUIESCENT"),
("TARGET_QUIESCENT", "SWITCH_NOT_AUTHORIZED"),
("SWITCH_NOT_AUTHORIZED", "SWITCH_AUTHORIZED"),
("SWITCH_AUTHORIZED", "SWITCH_IN_PROGRESS"),
("SWITCH_IN_PROGRESS", "POST_SWITCH_VERIFY"),
("POST_SWITCH_VERIFY", "MANUAL_EXECUTION_NOT_AUTHORIZED"),
("MANUAL_EXECUTION_NOT_AUTHORIZED", "MANUAL_EXECUTION_AUTHORIZED"),
("MANUAL_EXECUTION_AUTHORIZED", "ONE_SHOT_EXECUTION"),
("ONE_SHOT_EXECUTION", "ACCEPTED"),
("POST_SWITCH_VERIFY", "ROLLBACK_REQUIRED"),
("ROLLBACK_REQUIRED", "ROLLBACK_AUTHORIZED"),
("ROLLBACK_AUTHORIZED", "ROLLBACK_IN_PROGRESS"),
("ROLLBACK_IN_PROGRESS", "ROLLBACK_VERIFIED"),
}
COMPONENTS = {
"hardened_elfldr": {
"stock_size": 397000,
"stock_sha256": (
"092d16ee0ede0c494947efd38d1a17bbd7cc4b022d3858ea898833c188c703e8"
),
"candidate_size": 397000,
"candidate_sha256": (
"63e810982471eb40cae3a20aa9df9a0a02892f420e429874fae4e99aa400b561"
),
"candidate_source_commit": "197623058f509eddde18868dafcb92fdcac66464",
},
"controlled_payload_manager": {
"stock_size": 2050320,
"stock_sha256": (
"518740adbacccb9094fadb07dd424c53ee290f38306449ccc9d6957fdf813c0b"
),
"candidate_size": 99560,
"candidate_sha256": (
"8fecf8241a46246eddbd21e8bb4d875f5d76f1f4f4c6a11384df1f131aa5e5b1"
),
"candidate_source_commit": "e23d94ff91233aa770e2342800c1467875bdef44",
},
}
FAULTS = (
"wrong_preimage_hash",
"wrong_size",
"missing_live_path",
"missing_mount_id",
"missing_object_id",
"reference_only_preimage",
"object_swap_after_preflight",
"symlink_substitution",
"backup_same_object_as_live",
"short_backup_write",
"backup_hash_mismatch",
"backup_not_reopened",
"insufficient_space",
"candidate_hash_mismatch",
"target_process_active",
"autoload_active",
"retry_active",
"switch_primitive_unknown",
"atomic_switch_failure",
"directory_durability_unknown",
"live_verification_failure",
"rollback_hash_mismatch",
"recovery_depends_on_replaced_component",
"wrong_component_artifact_mapping",
"lifecycle_probe_candidate",
"authorization_missing",
"authorization_wrong_hash",
"authorization_expired",
"second_component_before_first_accepted",
"timeout",
"unknown_firmware",
"in_place_overwrite",
"two_step_rename_gap",
)
POWER_LOSS_BOUNDARIES = (
"before_backup_write",
"during_backup_write",
"after_backup_write_before_flush",
"after_flush_before_reopen_hash",
"during_candidate_staging",
"after_candidate_staging_before_verification",
"immediately_before_live_switch",
"during_live_switch",
"immediately_after_live_switch",
"after_switch_before_live_hash",
"after_live_hash_before_execution",
"during_first_manual_execution",
"during_rollback",
"after_rollback_before_recovery_verification",
)
@dataclass
class VirtualObject:
"""Logical file identity; it is never materialized on the host."""
path_token: str | None
mount_id: str | None
object_id: str | None
object_type: str
size: int
sha256: str
complete: bool = True
durable: bool = True
reopened: bool = False
@dataclass
class SyntheticAuthorization:
"""A host-model gate token that explicitly grants no real-world authority."""
action: str
component: str
artifact_sha256: str
gate_token_present: bool
expires_at_tick: int
synthetic_host_model_only: bool = True
def valid(
self, action: str, component: str, artifact_sha256: str, now_tick: int
) -> bool:
return (
self.synthetic_host_model_only
and self.gate_token_present
and self.action == action
and self.component == component
and self.artifact_sha256 == artifact_sha256
and now_tick < self.expires_at_tick
)
class TransitionError(RuntimeError):
"""Raised when the state machine would skip a mandatory gate."""
class StateMachine:
def __init__(self) -> None:
self.state = "OFFLINE_ONLY"
self.history = [self.state]
def transition(self, destination: str) -> None:
if destination not in STATES:
raise TransitionError(f"unknown state: {destination}")
if destination == "BLOCKED":
self.state = destination
self.history.append(destination)
return
if (self.state, destination) not in ALLOWED_FORWARD_TRANSITIONS:
raise TransitionError(
f"forbidden transition: {self.state} -> {destination}"
)
self.state = destination
self.history.append(destination)
def _authorization(
action: str,
component: str,
digest: str,
faults: set[str],
) -> SyntheticAuthorization:
gate_token_present = "authorization_missing" not in faults
scoped_digest = "0" * 64 if "authorization_wrong_hash" in faults else digest
expiry = 0 if "authorization_expired" in faults else 100
return SyntheticAuthorization(
action=action,
component=component,
artifact_sha256=scoped_digest,
gate_token_present=gate_token_present,
expires_at_tick=expiry,
)
def _base_objects(component: str) -> tuple[VirtualObject, VirtualObject]:
identity = COMPONENTS[component]
live = VirtualObject(
path_token=f"LIVE_SLOT::{component}",
mount_id=f"MOUNT::{component}",
object_id=f"OBJECT::stock::{component}",
object_type="regular",
size=identity["stock_size"],
sha256=identity["stock_sha256"],
)
candidate = VirtualObject(
path_token=f"STAGING_SLOT::{component}",
mount_id=f"MOUNT::{component}",
object_id=f"OBJECT::candidate::{component}",
object_type="regular",
size=identity["candidate_size"],
sha256=identity["candidate_sha256"],
)
return live, candidate
def _report(
machine: StateMachine,
component: str,
outcome: str,
blockers: list[dict[str, str]],
live: VirtualObject,
backup: VirtualObject | None,
candidate: VirtualObject,
writes: list[str],
risk: str | None = None,
) -> dict[str, Any]:
identity = COMPONENTS[component]
old_live_intact = (
live.complete
and live.object_type == "regular"
and live.path_token == f"LIVE_SLOT::{component}"
and live.mount_id == f"MOUNT::{component}"
and live.object_id
in {
f"OBJECT::stock::{component}",
f"OBJECT::restored::{component}",
}
and live.sha256 == identity["stock_sha256"]
and live.size == identity["stock_size"]
)
verified_new_live = (
live.complete
and live.object_type == "regular"
and live.path_token == f"LIVE_SLOT::{component}"
and live.mount_id == f"MOUNT::{component}"
and live.object_id == f"OBJECT::live-candidate::{component}"
and live.sha256 == identity["candidate_sha256"]
and live.size == identity["candidate_size"]
)
verified_backup_intact = (
backup is not None
and backup.complete
and backup.reopened
and backup.sha256 == identity["stock_sha256"]
and backup.size == identity["stock_size"]
and backup.object_id != live.object_id
)
crash_invariant = (
"A_OLD_LIVE_COMPLETE"
if old_live_intact
else (
"B_NEW_LIVE_COMPLETE_AND_BACKUP_INTACT"
if verified_new_live and verified_backup_intact
else "C_REJECTED_UNSAFE_OR_UNPROVEN"
)
)
return {
"schema_contract": "chimera-gfx-phase-0.9-host-simulator-v1",
"phase": "PHASE_0_9A_OFFLINE_ANTI_BRICK",
"status": outcome,
"component": component,
"state": machine.state,
"state_history": machine.history,
"blockers": blockers,
"risk": risk,
"virtual_writes": writes,
"virtual_live": asdict(live),
"virtual_backup": asdict(backup) if backup is not None else None,
"virtual_candidate": asdict(candidate),
"crash_invariant": crash_invariant,
"host_simulation_only": True,
"hardware_evidence": False,
"ps5_connected": False,
"device_write_performed": False,
"files_transferred": False,
"target_execution_performed": False,
"target_artifact_created": False,
"production_installation_code": False,
"synthetic_host_model_authorization_states": [
state
for state in machine.history
if state
in {
"OBSERVATION_AUTHORIZED",
"BACKUP_CREATION_AUTHORIZED",
"STAGING_AUTHORIZED",
"SWITCH_AUTHORIZED",
"MANUAL_EXECUTION_AUTHORIZED",
"ROLLBACK_AUTHORIZED",
}
],
"real_world_authority": False,
"installation_authorized": False,
"lifecycle_authorized": False,
"execution_authorized": False,
"transfer_authorized": False,
"automatic_retry": False,
"autoload": False,
}
def _blocked(
machine: StateMachine,
component: str,
code: str,
detail: str,
live: VirtualObject,
backup: VirtualObject | None,
candidate: VirtualObject,
writes: list[str],
risk: str = "HIGH",
) -> dict[str, Any]:
machine.transition("BLOCKED")
return _report(
machine,
component,
"BLOCKED",
[{"code": code, "detail": detail}],
live,
backup,
candidate,
writes,
risk,
)
def simulate_transaction(
component: str,
faults: Iterable[str] = (),
*,
prove_virtual_switch_model: bool = False,
) -> dict[str, Any]:
"""Run one component transaction entirely in memory.
``prove_virtual_switch_model`` proves only the synthetic model primitive.
It never promotes a PS5 filesystem property.
"""
if component not in COMPONENTS:
raise ValueError("component must be hardened_elfldr or controlled_payload_manager")
fault_set = set(faults)
unknown = fault_set.difference(FAULTS)
if unknown:
raise ValueError(f"unknown fault(s): {', '.join(sorted(unknown))}")
identity = COMPONENTS[component]
live, candidate = _base_objects(component)
expected = copy.deepcopy(live)
backup: VirtualObject | None = None
writes: list[str] = []
machine = StateMachine()
machine.transition("OBSERVATION_NOT_AUTHORIZED")
observation_auth = _authorization(
"observation", component, identity["stock_sha256"], fault_set
)
if not observation_auth.valid(
"observation", component, identity["stock_sha256"], now_tick=1
):
return _blocked(
machine,
component,
"OBSERVATION_AUTHORIZATION_INVALID",
"The synthetic observation input is missing, expired or hash-mismatched.",
live,
backup,
candidate,
writes,
)
machine.transition("OBSERVATION_AUTHORIZED")
if "unknown_firmware" in fault_set:
return _blocked(
machine,
component,
"UNKNOWN_FIRMWARE",
"An unknown firmware always stops before mutation.",
live,
backup,
candidate,
writes,
)
machine.transition("DEVICE_IDENTITY_OBSERVED")
if "missing_live_path" in fault_set:
live.path_token = None
if "missing_mount_id" in fault_set:
live.mount_id = None
if "missing_object_id" in fault_set:
live.object_id = None
if "wrong_preimage_hash" in fault_set:
live.sha256 = "1" * 64
if "wrong_size" in fault_set:
live.size += 1
if "symlink_substitution" in fault_set:
live.object_type = "symlink"
if "reference_only_preimage" in fault_set:
return _blocked(
machine,
component,
"REFERENCE_ONLY_PREIMAGE",
"A stock reference was not promoted by an exact stable observation.",
live,
backup,
candidate,
writes,
)
exact_identity = all(
(
live.path_token == expected.path_token,
live.mount_id == expected.mount_id,
live.object_id == expected.object_id,
live.object_type == "regular",
live.size == expected.size,
live.sha256 == expected.sha256,
live.complete,
)
)
if not exact_identity:
return _blocked(
machine,
component,
"LIVE_IDENTITY_MISMATCH",
"Path, mount, object, type, size and hash must all match.",
live,
backup,
candidate,
writes,
)
machine.transition("LIVE_OBJECTS_VERIFIED")
machine.transition("BACKUP_NOT_PRESENT")
machine.transition("BACKUP_CREATION_NOT_AUTHORIZED")
backup_auth = _authorization(
"backup_creation", component, identity["stock_sha256"], fault_set
)
if not backup_auth.valid(
"backup_creation", component, identity["stock_sha256"], now_tick=1
):
return _blocked(
machine,
component,
"BACKUP_AUTHORIZATION_INVALID",
"Missing, expired or differently hash-bound backup authorization.",
live,
backup,
candidate,
writes,
)
machine.transition("BACKUP_CREATION_AUTHORIZED")
if "object_swap_after_preflight" in fault_set:
live.object_id = "OBJECT::substituted"
return _blocked(
machine,
component,
"OBJECT_CHANGED_AFTER_PREFLIGHT",
"The stable live object identity changed before the virtual write.",
live,
backup,
candidate,
writes,
)
if "insufficient_space" in fault_set:
return _blocked(
machine,
component,
"INSUFFICIENT_SPACE",
"Capacity and metadata reserve are insufficient.",
live,
backup,
candidate,
writes,
)
if "backup_same_object_as_live" in fault_set:
backup = copy.deepcopy(live)
return _blocked(
machine,
component,
"BACKUP_NOT_SEPARATE",
"The backup resolves to the live object.",
live,
backup,
candidate,
writes,
"CATASTROPHIC",
)
backup = VirtualObject(
path_token=f"BACKUP_SLOT::{component}",
mount_id=f"BACKUP_MOUNT::{component}",
object_id=f"OBJECT::backup::{component}",
object_type="regular",
size=live.size,
sha256=live.sha256,
durable=True,
)
writes.append("virtual_backup_create")
machine.transition("BACKUP_CREATED")
if "short_backup_write" in fault_set:
backup.size -= 1
backup.complete = False
return _blocked(
machine,
component,
"SHORT_BACKUP_WRITE",
"A partial backup is never promotable.",
live,
backup,
candidate,
writes,
"CATASTROPHIC",
)
if "backup_hash_mismatch" in fault_set:
backup.sha256 = "2" * 64
if "backup_not_reopened" not in fault_set:
backup.reopened = True
if (
backup.sha256 != expected.sha256
or backup.size != expected.size
or not backup.reopened
):
return _blocked(
machine,
component,
"BACKUP_REOPEN_VERIFY_FAILED",
"The separate backup must survive close, reopen, size and hash checks.",
live,
backup,
candidate,
writes,
"CATASTROPHIC",
)
machine.transition("BACKUP_REOPENED_AND_VERIFIED")
if "recovery_depends_on_replaced_component" in fault_set:
return _blocked(
machine,
component,
"RECOVERY_NOT_INDEPENDENT",
"Recovery depends on the component being replaced.",
live,
backup,
candidate,
writes,
"CATASTROPHIC",
)
machine.transition("RECOVERY_PATH_VERIFIED")
machine.transition("CANDIDATE_NOT_AUTHORIZED")
if "lifecycle_probe_candidate" in fault_set:
return _blocked(
machine,
component,
"LIFECYCLE_PROBE_NOT_INSTALLABLE",
"The lifecycle probe is excluded from both component transactions.",
live,
backup,
candidate,
writes,
)
if "wrong_component_artifact_mapping" in fault_set:
return _blocked(
machine,
component,
"COMPONENT_ARTIFACT_MAPPING_MISMATCH",
"The candidate is bound to a different component.",
live,
backup,
candidate,
writes,
)
if "second_component_before_first_accepted" in fault_set:
return _blocked(
machine,
component,
"COMPONENT_SEQUENCE_VIOLATION",
"A second component cannot start before separate acceptance or rollback.",
live,
backup,
candidate,
writes,
)
machine.transition("CANDIDATE_APPROVED_OFFLINE")
machine.transition("STAGING_NOT_AUTHORIZED")
stage_auth = _authorization(
"staging", component, identity["candidate_sha256"], fault_set
)
if not stage_auth.valid(
"staging", component, identity["candidate_sha256"], now_tick=1
):
return _blocked(
machine,
component,
"STAGING_AUTHORIZATION_INVALID",
"Staging approval is missing, expired or bound to different bytes.",
live,
backup,
candidate,
writes,
)
machine.transition("STAGING_AUTHORIZED")
if "candidate_hash_mismatch" in fault_set:
candidate.sha256 = "3" * 64
writes.append("virtual_candidate_stage")
machine.transition("CANDIDATE_STAGED")
candidate.reopened = True
if (
candidate.sha256 != identity["candidate_sha256"]
or candidate.size != identity["candidate_size"]
or not candidate.complete
or not candidate.reopened
):
return _blocked(
machine,
component,
"CANDIDATE_REOPEN_VERIFY_FAILED",
"Staged candidate bytes do not match the offline approved artifact.",
live,
backup,
candidate,
writes,
)
machine.transition("CANDIDATE_REOPENED_AND_VERIFIED")
machine.transition("TARGET_NOT_QUIESCENT")
if "target_process_active" in fault_set:
return _blocked(
machine,
component,
"TARGET_NOT_QUIESCENT",
"The target process or service is still active.",
live,
backup,
candidate,
writes,
)
if "autoload_active" in fault_set:
return _blocked(
machine,
component,
"AUTOLOAD_ACTIVE",
"Autoload is forbidden.",
live,
backup,
candidate,
writes,
)
if "retry_active" in fault_set:
return _blocked(
machine,
component,
"AUTOMATIC_RETRY_ACTIVE",
"Automatic retry is forbidden.",
live,
backup,
candidate,
writes,
)
if "timeout" in fault_set:
return _blocked(
machine,
component,
"TIMEOUT_STOP",
"A timeout stops and never retries.",
live,
backup,
candidate,
writes,
)
machine.transition("TARGET_QUIESCENT")
machine.transition("SWITCH_NOT_AUTHORIZED")
switch_auth = _authorization(
"switch", component, identity["candidate_sha256"], fault_set
)
if not switch_auth.valid(
"switch", component, identity["candidate_sha256"], now_tick=1
):
return _blocked(
machine,
component,
"SWITCH_AUTHORIZATION_INVALID",
"Switch approval is separate and hash-bound.",
live,
backup,
candidate,
writes,
)
machine.transition("SWITCH_AUTHORIZED")
platform_unproven = (
not prove_virtual_switch_model
or "switch_primitive_unknown" in fault_set
or "directory_durability_unknown" in fault_set
or "in_place_overwrite" in fault_set
or "two_step_rename_gap" in fault_set
)
if platform_unproven:
return _blocked(
machine,
component,
"NO_PROVEN_POWER_LOSS_SAFE_SWITCH",
"Atomicity and directory durability are not proven for the platform.",
live,
backup,
candidate,
writes,
"CATASTROPHIC",
)
machine.transition("SWITCH_IN_PROGRESS")
if "atomic_switch_failure" in fault_set:
return _blocked(
machine,
component,
"ATOMIC_SWITCH_FAILED",
"The virtual atomic switch failed and left the old live object intact.",
live,
backup,
candidate,
writes,
"CATASTROPHIC",
)
live = copy.deepcopy(candidate)
live.path_token = expected.path_token
live.object_id = f"OBJECT::live-candidate::{component}"
writes.append("virtual_atomic_switch")
machine.transition("POST_SWITCH_VERIFY")
if "live_verification_failure" in fault_set:
machine.transition("ROLLBACK_REQUIRED")
return _report(
machine,
component,
"ROLLBACK_REQUIRED",
[
{
"code": "POST_SWITCH_VERIFY_FAILED",
"detail": (
"A separate rollback authorization is required; "
"execution remains forbidden."
),
}
],
live,
backup,
candidate,
writes,
"CATASTROPHIC",
)
if "rollback_hash_mismatch" in fault_set:
machine.transition("ROLLBACK_REQUIRED")
rollback_auth = _authorization(
"rollback", component, identity["stock_sha256"], set()
)
if not rollback_auth.valid(
"rollback", component, identity["stock_sha256"], now_tick=1
):
return _blocked(
machine,
component,
"ROLLBACK_AUTHORIZATION_INVALID",
"Rollback authorization is absent.",
live,
backup,
candidate,
writes,
"CATASTROPHIC",
)
machine.transition("ROLLBACK_AUTHORIZED")
machine.transition("ROLLBACK_IN_PROGRESS")
live = copy.deepcopy(backup)
live.path_token = expected.path_token
live.object_id = f"OBJECT::restored::{component}"
writes.append("virtual_atomic_rollback")
if "rollback_hash_mismatch" in fault_set:
live.sha256 = "4" * 64
return _blocked(
machine,
component,
"ROLLBACK_VERIFY_FAILED",
"Rollback hash mismatch is catastrophic and remains blocked.",
live,
backup,
candidate,
writes,
"CATASTROPHIC",
)
machine.transition("ROLLBACK_VERIFIED")
return _report(
machine,
component,
"ROLLBACK_VERIFIED",
[],
live,
backup,
candidate,
writes,
)
machine.transition("MANUAL_EXECUTION_NOT_AUTHORIZED")
return _report(
machine,
component,
"DESIGN_MODEL_STOP_BEFORE_EXECUTION",
[
{
"code": "MANUAL_EXECUTION_NOT_AUTHORIZED",
"detail": "Execution is a later separate gate and is not simulated here.",
}
],
live,
backup,
candidate,
writes,
)
def simulate_power_loss_boundary(
component: str, boundary: str, *, prove_virtual_switch_model: bool = False
) -> dict[str, Any]:
"""Classify one abstract interruption without performing any real write."""
if component not in COMPONENTS:
raise ValueError("unknown component")
if boundary not in POWER_LOSS_BOUNDARIES:
raise ValueError("unknown power-loss boundary")
if not prove_virtual_switch_model:
return {
"boundary": boundary,
"result": "UNPROVEN",
"implementation_blocked": True,
"reason": "PS5 atomicity and durability are not proven",
"host_simulation_only": True,
"hardware_evidence": False,
"automatic_start": False,
"automatic_retry": False,
"crash_invariant": "C_REJECTED_UNSAFE_OR_UNPROVEN",
}
after_switch = boundary in {
"immediately_after_live_switch",
"after_switch_before_live_hash",
"after_live_hash_before_execution",
"during_first_manual_execution",
"during_rollback",
}
return {
"boundary": boundary,
"result": "SAFE_IN_VIRTUAL_MODEL_ONLY",
"implementation_blocked": False,
"reason": "the synthetic atomic primitive chooses one complete identity",
"host_simulation_only": True,
"hardware_evidence": False,
"automatic_start": False,
"automatic_retry": False,
"crash_invariant": (
"B_NEW_LIVE_COMPLETE_AND_BACKUP_INTACT"
if after_switch
else "A_OLD_LIVE_COMPLETE"
),
}
def run_fault_suite(component: str) -> dict[str, Any]:
results: list[dict[str, Any]] = []
for fault in FAULTS:
report = simulate_transaction(
component,
[fault],
prove_virtual_switch_model=True,
)
results.append(
{
"fault": fault,
"status": report["status"],
"state": report["state"],
"risk": report["risk"],
"crash_invariant": report["crash_invariant"],
"target_execution_performed": report[
"target_execution_performed"
],
"automatic_retry": report["automatic_retry"],
}
)
power_loss = [
simulate_power_loss_boundary(
component, boundary, prove_virtual_switch_model=True
)
for boundary in POWER_LOSS_BOUNDARIES
]
return {
"schema_contract": "chimera-gfx-phase-0.9-host-fault-suite-v1",
"phase": "PHASE_0_9A_OFFLINE_ANTI_BRICK",
"component": component,
"fault_results": results,
"power_loss_results": power_loss,
"host_simulation_only": True,
"hardware_evidence": False,
"ps5_connected": False,
"device_write_performed": False,
"files_transferred": False,
"target_execution_performed": False,
"target_artifact_created": False,
"automatic_retry": False,
}
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument(
"--component",
choices=tuple(COMPONENTS),
required=True,
)
parser.add_argument("--fault", choices=FAULTS, action="append", default=[])
parser.add_argument(
"--power-loss-boundary",
choices=POWER_LOSS_BOUNDARIES,
)
parser.add_argument("--run-fault-suite", action="store_true")
args = parser.parse_args()
if args.run_fault_suite and (args.fault or args.power_loss_boundary):
parser.error("--run-fault-suite cannot be combined with another scenario")
if args.power_loss_boundary and args.fault:
parser.error("--power-loss-boundary cannot be combined with --fault")
if args.run_fault_suite:
report = run_fault_suite(args.component)
elif args.power_loss_boundary:
report = simulate_power_loss_boundary(
args.component, args.power_loss_boundary
)
else:
report = simulate_transaction(args.component, args.fault)
print(json.dumps(report, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())