Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Harden governed scoring execution integer boundaries

## Fixed

- Reject caller-defined integer coercion at governed scoring request, observation, and result controls before any `__index__` callback can run, while preserving exact built-in and genuine NumPy integer scalar compatibility and existing bounded `AssessmentSpecError` semantics.
76 changes: 76 additions & 0 deletions python/fast_mlsirm/scoring/_execution_integer_safety.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Inert integer normalization for governed scoring execution contracts.

This composition layer exists because execution contracts are imported through the
stable scoring surface while their validation helpers remain private. It installs
exact-type integer normalizers before any public execution constructor is exposed,
so rejected caller values cannot dispatch conversion or comparison callbacks.
"""

from __future__ import annotations

from types import ModuleType
from typing import Any

import numpy as np

from ._validation import assessment_error

_NUMPY_INTEGER_SCALAR_TYPES = (
np.int8,
np.int16,
np.int32,
np.int64,
np.intp,
np.longlong,
np.uint8,
np.uint16,
np.uint32,
np.uint64,
np.uintp,
np.ulonglong,
)


def _trusted_integer(value: Any, name: str, message: str) -> int:
"""Return an exact trusted integer without caller-controlled coercion."""
value_type = type(value)
if value_type is int:
return value
if any(value_type is scalar_type for scalar_type in _NUMPY_INTEGER_SCALAR_TYPES):
return int(value)
raise assessment_error(
f"invalid_{name}",
f"$.{name}",
message,
)


def _nonnegative_integer(value: Any, name: str, maximum: int) -> int:
"""Return one bounded nonnegative trusted integer control."""
normalized = _trusted_integer(
value,
name,
f"{name} must be an integer between 0 and {maximum}",
)
if not 0 <= normalized <= maximum:
raise assessment_error(
f"invalid_{name}",
f"$.{name}",
f"{name} must be between 0 and {maximum}",
)
return normalized


def _score_integer(value: Any, name: str = "score_category") -> int:
"""Return one trusted integer score category without protocol dispatch."""
return _trusted_integer(
value,
name,
f"{name} must be an integer",
)


def install(execution_module: ModuleType) -> None:
"""Install inert integer normalizers on the package-owned execution module."""
execution_module._nonnegative_integer = _nonnegative_integer
execution_module._score_integer = _score_integer
5 changes: 5 additions & 0 deletions python/fast_mlsirm/scoring/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
components.
"""

from . import execution as _execution
from ._execution_integer_safety import install as _install_execution_integer_safety

_install_execution_integer_safety(_execution)

from ._contract_safety import artifact_digest as artifact_digest
from ._contract_safety import canonical_json as canonical_json
from ._validation import ASSESSMENT_SCHEMA_VERSION as ASSESSMENT_SCHEMA_VERSION
Expand Down
243 changes: 243 additions & 0 deletions tests/test_scoring_execution_integer_callback_safety.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
"""Regression contracts for inert governed-scoring integer validation."""

from __future__ import annotations

from pathlib import Path
import runpy

import numpy as np
import pytest

from fast_mlsirm.scoring import (
AssessmentSpecError,
ObservationStatus,
build_score_observation,
build_scoring_result,
)

_FIXTURES = runpy.run_path(
str(Path(__file__).with_name("scoring_execution_fixtures.py"))
)
automated_engine = _FIXTURES["automated_engine"]
criterion_request = _FIXTURES["criterion_request"]
fixture_engine = _FIXTURES["fixture_engine"]


class _IndexCallback:
"""Integer-like caller value whose numeric hooks are executable code."""

def __init__(self, value: int) -> None:
self.value = value
self.callback_count = 0

def __int__(self) -> int:
self.callback_count += 1
return self.value

def __index__(self) -> int:
self.callback_count += 1
return self.value

def __repr__(self) -> str:
self.callback_count += 1
return str(self.value)

def __eq__(self, other: object) -> bool:
self.callback_count += 1
return False

def __hash__(self) -> int:
self.callback_count += 1
return self.value

def __lt__(self, other: object) -> bool:
self.callback_count += 1
return False

def __le__(self, other: object) -> bool:
self.callback_count += 1
return False

def __gt__(self, other: object) -> bool:
self.callback_count += 1
return False

def __ge__(self, other: object) -> bool:
self.callback_count += 1
return False


class _CallerInt(int):
"""Caller-defined built-in integer subclass with executable numeric hooks."""

callback_count = 0

def __int__(self) -> int:
type(self).callback_count += 1
return 1

def __index__(self) -> int:
type(self).callback_count += 1
return 1

def __repr__(self) -> str:
type(self).callback_count += 1
return "1"

def __eq__(self, other: object) -> bool:
type(self).callback_count += 1
return False

def __hash__(self) -> int:
type(self).callback_count += 1
return 1

def __lt__(self, other: object) -> bool:
type(self).callback_count += 1
return False

def __le__(self, other: object) -> bool:
type(self).callback_count += 1
return False

def __gt__(self, other: object) -> bool:
type(self).callback_count += 1
return False

def __ge__(self, other: object) -> bool:
type(self).callback_count += 1
return False


class _CallerNumpyInt(np.int64):
"""Caller-defined NumPy integer subclass with executable numeric hooks."""

callback_count = 0

def __int__(self) -> int:
type(self).callback_count += 1
return 2

def __index__(self) -> int:
type(self).callback_count += 1
return 2

def __repr__(self) -> str:
type(self).callback_count += 1
return "2"

def __eq__(self, other: object) -> bool:
type(self).callback_count += 1
return False

def __hash__(self) -> int:
type(self).callback_count += 1
return 2

def __lt__(self, other: object) -> bool:
type(self).callback_count += 1
return False

def __le__(self, other: object) -> bool:
type(self).callback_count += 1
return False

def __gt__(self, other: object) -> bool:
type(self).callback_count += 1
return False

def __ge__(self, other: object) -> bool:
type(self).callback_count += 1
return False


def _scored_observations():
"""Return a complete trusted observation set for one criterion request."""
request = criterion_request()
return request, fixture_engine().score(request).observations


def test_request_integer_controls_reject_index_callbacks_without_execution() -> None:
"""Request controls fail before conversion, comparison, equality, or hashing."""
hostile = _IndexCallback(128)

with pytest.raises(AssessmentSpecError) as captured:
criterion_request(response_character_count=hostile)

assert captured.value.code == "invalid_response_character_count"
assert hostile.callback_count == 0


def test_score_category_rejects_numpy_subclasses_without_execution() -> None:
"""Score categories reject NumPy subclasses before any numeric callback."""
request = criterion_request()
engine = automated_engine()
_CallerNumpyInt.callback_count = 0

with pytest.raises(AssessmentSpecError) as captured:
build_score_observation(
observation_id="hostile_score_observation",
request=request,
engine=engine,
criterion_id="claim_support",
status=ObservationStatus.SCORED,
score_category=_CallerNumpyInt(2),
)

assert captured.value.code == "invalid_score_category"
assert _CallerNumpyInt.callback_count == 0


def test_execution_attempt_rejects_python_integer_subclasses_without_execution() -> None:
"""Result attempt counters reject Python subclasses before numeric callbacks."""
request, observations = _scored_observations()
_CallerInt.callback_count = 0

with pytest.raises(AssessmentSpecError) as captured:
build_scoring_result(
result_id="hostile_attempt_result",
request=request,
engine=automated_engine(),
observations=observations,
execution_attempt=_CallerInt(1),
)

assert captured.value.code == "invalid_execution_attempt"
assert _CallerInt.callback_count == 0


def test_genuine_numpy_integer_scalars_remain_supported() -> None:
"""Trusted NumPy scalar identities preserve the existing public contract."""
request = criterion_request(
response_character_count=np.int64(128),
response_unit_count=np.uint32(8),
)
engine = automated_engine()
observation = build_score_observation(
observation_id="numpy_score_observation",
request=request,
engine=engine,
criterion_id="claim_support",
status=ObservationStatus.SCORED,
score_category=np.int16(2),
)
second = build_score_observation(
observation_id="numpy_alignment_observation",
request=request,
engine=engine,
criterion_id="source_alignment",
status=ObservationStatus.SCORED,
score_category=np.uint8(1),
)
result = build_scoring_result(
result_id="numpy_attempt_result",
request=request,
engine=engine,
observations=(observation, second),
execution_attempt=np.uint64(1),
)

assert request.response_character_count == 128
assert request.response_unit_count == 8
assert observation.score_category == 2
assert result.execution_attempt == 1
Loading