From 0580fee690fa03e750e7284fa1700e5df2aba197 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 13:43:01 +0900 Subject: [PATCH 1/2] test(multilevel): expose hostile numeric callback leak --- ...st_multilevel_hostile_numeric_callbacks.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/test_multilevel_hostile_numeric_callbacks.py diff --git a/tests/test_multilevel_hostile_numeric_callbacks.py b/tests/test_multilevel_hostile_numeric_callbacks.py new file mode 100644 index 000000000..983ae3593 --- /dev/null +++ b/tests/test_multilevel_hostile_numeric_callbacks.py @@ -0,0 +1,53 @@ +"""Hostile numeric callback boundaries for multilevel contracts.""" + +from __future__ import annotations + +import pytest + +from fast_mlsirm.multilevel import ( + LongitudinalStateKind, + MultilevelContractError, + build_context_membership, + build_longitudinal_state_spec, +) + + +class ExplosiveFloat(float): + """Float subclass that exposes whether validation dispatches callbacks.""" + + def __float__(self) -> float: + """Raise an attacker-controlled exception instead of coercing.""" + raise RuntimeError("sensitive_numeric_callback") + + +def test_membership_weight_rejects_numeric_subclass_without_callback() -> None: + """Membership validation fails closed before untrusted numeric coercion.""" + value = ExplosiveFloat(1.0) + + with pytest.raises(MultilevelContractError) as caught: + build_context_membership( + observation_id="observation_alpha", + context_dimension_id="school_context", + context_id="school_north", + membership_weight=value, + membership_revision_fingerprint="a" * 64, + ) + + assert caught.value.code == "invalid_membership_weight" + assert caught.value.path == "$.membership_weight" + assert "sensitive_numeric_callback" not in str(caught.value) + + +def test_autoregressive_coefficient_rejects_numeric_subclass_without_callback() -> None: + """AR-state validation fails closed before untrusted numeric coercion.""" + value = ExplosiveFloat(0.5) + + with pytest.raises(MultilevelContractError) as caught: + build_longitudinal_state_spec( + state_kind=LongitudinalStateKind.STATIONARY_AUTOREGRESSIVE, + autoregressive_coefficient=value, + ) + + assert caught.value.code == "invalid_autoregressive_coefficient" + assert caught.value.path == "$.autoregressive_coefficient" + assert "sensitive_numeric_callback" not in str(caught.value) From 35bb61f7d70d1ba4993d3bb2f9397ce5b7e7b5a0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 13:50:20 +0900 Subject: [PATCH 2/2] fix(multilevel): reject numeric coercion callbacks --- python/fast_mlsirm/multilevel/_validation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/fast_mlsirm/multilevel/_validation.py b/python/fast_mlsirm/multilevel/_validation.py index c3f7d533c..f5a8779dc 100644 --- a/python/fast_mlsirm/multilevel/_validation.py +++ b/python/fast_mlsirm/multilevel/_validation.py @@ -114,7 +114,7 @@ def exact_integer( def membership_weight(value: Any) -> float: """Return one finite membership weight in the interval ``(0, 1]``.""" - if isinstance(value, bool) or not isinstance(value, (int, float)): + if type(value) not in (int, float): raise contract_error( "invalid_membership_weight", "$.membership_weight", @@ -132,7 +132,7 @@ def membership_weight(value: Any) -> float: def autoregressive_coefficient(value: Any) -> float: """Return one finite stationary AR(1) coefficient strictly inside unity.""" - if isinstance(value, bool) or not isinstance(value, (int, float)): + if type(value) not in (int, float): raise contract_error( "invalid_autoregressive_coefficient", "$.autoregressive_coefficient",