diff --git a/tests/test_execution_history_events.py b/tests/test_execution_history_events.py index 0aedd71..76a1474 100644 --- a/tests/test_execution_history_events.py +++ b/tests/test_execution_history_events.py @@ -344,3 +344,36 @@ class ExecutionHistoryEventsTests(unittest.TestCase): classify_evidence(JOB, command.evidence).reason, "evidence_limit" ) self.assertLessEqual(len(encode_command(command)), 2048) + + def test_oversized_unknown_kind_is_ignored_without_hashing(self): + class OversizedKind(str): + def __hash__(self): + raise AssertionError("Oversized event kind must not be hashed") + + command = finish( + [ + message(OversizedKind("x" * 1_000_000)), + message("execution_success", timestamp=300), + ] + ) + result = classify_evidence(JOB, command.evidence) + self.assertEqual( + (result.outcome, result.reason), ("succeeded", "terminal_success") + ) + self.assertLessEqual(len(encode_command(command)), 2048) + + def test_encoder_rejects_oversized_kind_without_hashing(self): + class OversizedKind(str): + def __hash__(self): + raise AssertionError("Oversized event kind must not be hashed") + + command = finish([message("execution_success")]) + event = LifecycleEvent( + OversizedKind("x" * 1_000_000), JOB, None, None, None, None + ) + with self.assertRaises(ValueError): + encode_command( + replace( + command, evidence=TerminalEvidence((event,), False, False, False) + ) + ) diff --git a/utils/execution_history_events.py b/utils/execution_history_events.py index 5be4f6b..91eb4c1 100644 --- a/utils/execution_history_events.py +++ b/utils/execution_history_events.py @@ -13,6 +13,7 @@ TERMINAL_KINDS = { "execution_interrupted": ("interrupted", "terminal_interrupted"), } EVENT_KINDS = frozenset((*TERMINAL_KINDS, "execution_start")) +MAX_EVENT_KIND_LENGTH = max(map(len, EVENT_KINDS)) @dataclass(frozen=True) @@ -68,6 +69,14 @@ class Completion: RunCommand = RunStart | RunFinish +def _valid_event_kind(value: object) -> bool: + return ( + isinstance(value, str) + and len(value) <= MAX_EVENT_KIND_LENGTH + and value in EVENT_KINDS + ) + + def _valid_integer(value: object) -> bool: return type(value) is int and 0 <= value <= MAX_INTEGER @@ -159,7 +168,7 @@ def make_finish( if not malformed: kind, data = message malformed = not isinstance(kind, str) - if not malformed and kind not in EVENT_KINDS: + if not malformed and not _valid_event_kind(kind): continue malformed |= usable_job_id is None if not malformed: @@ -302,8 +311,7 @@ def _valid_evidence(evidence: object) -> bool: if type(event) is not LifecycleEvent: return False if ( - not isinstance(event.kind, str) - or event.kind not in EVENT_KINDS + not _valid_event_kind(event.kind) or not _valid_uuid(event.job_id) or (event.timestamp is not None and not _valid_integer(event.timestamp)) ):