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
7 changes: 7 additions & 0 deletions docs/changelog.d/974-utility-control-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Selection utility numeric trust boundary

## Fixed

- Hardened classical selection-utility and Taylor-Russell scalar controls so booleans, non-real objects, and non-finite values fail with package-owned validation before compiled Rust discovery.
- Prevented arbitrary caller-defined `__float__` callbacks from executing during public control marshalling while preserving genuine Python/NumPy real scalar compatibility and keeping all BCG, Naylor-Shine, and Taylor-Russell arithmetic Rust-owned.
- Normalized exact built-in integers outside the representable float range to the same package-owned validation error instead of leaking `OverflowError`.
54 changes: 52 additions & 2 deletions python/fast_mlsirm/utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,29 @@

from __future__ import annotations

import math
from dataclasses import dataclass

import numpy as np


_TRUSTED_REAL_SCALAR_TYPES = (
int,
float,
np.int8,
np.int16,
np.int32,
np.int64,
np.uint8,
np.uint16,
np.uint32,
np.uint64,
np.float16,
np.float32,
np.float64,
np.longdouble,
)


@dataclass
class SelectionUtilityResult:
Expand Down Expand Up @@ -67,6 +88,19 @@ class TaylorRussellResult:
q_joint: float


def _coerce_finite_real(value: object, *, name: str) -> float:
"""Return a trusted finite real scalar without invoking caller protocols."""
if type(value) not in _TRUSTED_REAL_SCALAR_TYPES:
raise ValueError(f"{name} must be a finite real number")
Comment thread
seonghobae marked this conversation as resolved.
try:
marshaled = float(value)
except OverflowError:
raise ValueError(f"{name} must be a finite real number") from None
if not math.isfinite(marshaled):
raise ValueError(f"{name} must be a finite real number")
return marshaled


def selection_utility(
n: float,
sdy: float,
Expand All @@ -84,10 +118,22 @@ def selection_utility(
applicant" but never multiplies by ``n``; we document the actual
semantics), ``period`` expected tenure (>= 1).
"""
marshaled_n = _coerce_finite_real(n, name="n")
marshaled_sdy = _coerce_finite_real(sdy, name="sdy")
marshaled_rxy = _coerce_finite_real(rxy, name="rxy")
marshaled_sr = _coerce_finite_real(sr, name="sr")
marshaled_cost_total = _coerce_finite_real(cost_total, name="cost_total")
marshaled_period = _coerce_finite_real(period, name="period")

from . import _core

r = _core.selection_utility(
float(n), float(sdy), float(rxy), float(sr), float(cost_total), float(period)
marshaled_n,
marshaled_sdy,
marshaled_rxy,
marshaled_sr,
marshaled_cost_total,
marshaled_period,
)
return SelectionUtilityResult(
xc=r["xc"], ux=r["ux"], pux=r["pux"], utility_gain=r["utility_gain"]
Expand All @@ -101,9 +147,13 @@ def taylor_russell(rxy: float, sr: float, br: float) -> TaylorRussellResult:
base rate of success in (0, 1). At ``rxy = 0`` the success ratio
equals ``br`` (no selection information).
"""
marshaled_rxy = _coerce_finite_real(rxy, name="rxy")
marshaled_sr = _coerce_finite_real(sr, name="sr")
marshaled_br = _coerce_finite_real(br, name="br")

from . import _core

r = _core.taylor_russell(float(rxy), float(sr), float(br))
r = _core.taylor_russell(marshaled_rxy, marshaled_sr, marshaled_br)
return TaylorRussellResult(
success_ratio=r["success_ratio"],
base_rate=r["base_rate"],
Expand Down
100 changes: 100 additions & 0 deletions tests/test_utility_control_boundary.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Trust-boundary regressions for classical selection utility controls."""

from __future__ import annotations

import math

import numpy as np
import pytest

from fast_mlsirm.utility import selection_utility, taylor_russell


class _HostileFloat:
"""Arbitrary float-protocol object whose callback must stay unreachable."""

calls = 0

@classmethod
def reset(cls) -> None:
"""Reset the callback counter."""
cls.calls = 0

def __float__(self) -> float:
type(self).calls += 1
raise AssertionError("caller __float__ callback must not execute")


class _HostileFloatSubclass(float):
"""Float subclass whose conversion callback must stay unreachable."""

calls = 0

@classmethod
def reset(cls) -> None:
"""Reset the callback counter."""
cls.calls = 0

def __float__(self) -> float:
type(self).calls += 1
raise AssertionError("caller float-subclass __float__ callback must not execute")


_UTILITY_CONTROL_CALLS = (
lambda value: selection_utility(value, 1.0, 0.5, 0.5),
lambda value: selection_utility(1.0, value, 0.5, 0.5),
lambda value: selection_utility(1.0, 1.0, value, 0.5),
lambda value: selection_utility(1.0, 1.0, 0.5, value),
lambda value: selection_utility(1.0, 1.0, 0.5, 0.5, value, 1.0),
lambda value: selection_utility(1.0, 1.0, 0.5, 0.5, 0.0, value),
lambda value: taylor_russell(value, 0.5, 0.5),
lambda value: taylor_russell(0.4, value, 0.5),
lambda value: taylor_russell(0.4, 0.5, value),
)


@pytest.mark.parametrize("call", _UTILITY_CONTROL_CALLS)
@pytest.mark.parametrize("hostile_type", (_HostileFloat, _HostileFloatSubclass))
def test_utility_rejects_float_protocol_objects_without_callbacks(
call, hostile_type
) -> None:
"""Every scalar position rejects hostile conversion without callback execution."""
hostile_type.reset()

with pytest.raises(ValueError, match="must be a finite real number"):
call(hostile_type())

assert hostile_type.calls == 0


@pytest.mark.parametrize("call", _UTILITY_CONTROL_CALLS)
@pytest.mark.parametrize("value", (True, False, math.inf, -math.inf, math.nan))
def test_utility_rejects_boolean_and_nonfinite_controls(call, value) -> None:
"""Every scalar position rejects boolean and non-finite control values."""
with pytest.raises(ValueError, match="must be a finite real number"):
call(value)


@pytest.mark.parametrize("call", _UTILITY_CONTROL_CALLS)
def test_utility_rejects_unrepresentable_builtin_integers(call) -> None:
"""Exact huge integers must become stable package-owned validation errors."""
with pytest.raises(ValueError, match="must be a finite real number"):
call(10**400)


def test_numpy_real_scalars_preserve_native_results() -> None:
"""Trusted NumPy real scalars produce the same Rust-owned results as floats."""
py_utility = selection_utility(10.0, 2.0, 0.4, 0.5, 1.0, 2.0)
np_utility = selection_utility(
np.float64(10.0),
np.float64(2.0),
np.float64(0.4),
np.float64(0.5),
np.float64(1.0),
np.float64(2.0),
)
assert np_utility == py_utility

py_tr = taylor_russell(0.4, 0.5, 0.6)
np_tr = taylor_russell(np.float64(0.4), np.float64(0.5), np.float64(0.6))
assert np_tr == py_tr
Loading