From 5d8965445c69dbd465a8a65d6201eb12d9d116d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 21:27:38 -0700 Subject: [PATCH 1/4] test(crm): expose zero-cell traversal budget gap --- tests/test_crm_response_admission.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_crm_response_admission.py b/tests/test_crm_response_admission.py index aef870631..9a1c2a41e 100644 --- a/tests/test_crm_response_admission.py +++ b/tests/test_crm_response_admission.py @@ -139,6 +139,26 @@ def unexpected_core() -> object: crm.fit_crm(responses) +def test_zero_cell_fanout_is_bounded_before_sequence_materialization(monkeypatch): + """Malformed empty-container fan-out is bounded independently of scalar cells.""" + + responses = [[] for _ in range(9)] + + def unexpected_asarray(*args, **kwargs): + del args, kwargs + raise AssertionError("NumPy materialization ran before CRM structural admission") + + def unexpected_core() -> object: + raise AssertionError("compiled core discovered before CRM structural admission") + + monkeypatch.setattr(crm, "_MAX_CRM_RESPONSE_STRUCTURAL_NODES", 8, raising=False) + monkeypatch.setattr(crm.np, "asarray", unexpected_asarray) + monkeypatch.setattr(fitstats, "_core_module", unexpected_core) + + with pytest.raises(ValueError, match="structural traversal budget"): + crm.fit_crm(responses) + + def test_builtin_response_matrix_preserves_trusted_numpy_scalar_marshalling(monkeypatch): """Ordinary built-in rows with concrete NumPy reals remain compatible.""" From 1e336ccfd273041e1a431e46053286099ebb5df2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 21:28:14 -0700 Subject: [PATCH 2/4] fix(crm): bound structural response traversal --- python/fast_mlsirm/crm.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/python/fast_mlsirm/crm.py b/python/fast_mlsirm/crm.py index 0e6c2d625..62f053291 100644 --- a/python/fast_mlsirm/crm.py +++ b/python/fast_mlsirm/crm.py @@ -12,6 +12,7 @@ _SUPPORTED_Q_THETA = (7, 11, 15, 21, 31, 41) _MAX_CRM_RESPONSE_CELLS = 20_000_000 +_MAX_CRM_RESPONSE_STRUCTURAL_NODES = 2 * _MAX_CRM_RESPONSE_CELLS _NUMPY_INTEGER_TYPES = tuple( np.dtype(name).type for name in ("int8", "int16", "int32", "int64", "uint8", "uint16", "uint32", "uint64") @@ -79,6 +80,7 @@ def _trusted_response_array(responses: object) -> np.ndarray: resource_error = ( f"responses must contain at most {_MAX_CRM_RESPONSE_CELLS:,} logical cells" ) + structural_error = "responses exceed structural traversal budget" if type(responses) is np.ndarray: if responses.size > _MAX_CRM_RESPONSE_CELLS: raise ValueError(resource_error) @@ -88,6 +90,11 @@ def _trusted_response_array(responses: object) -> np.ndarray: if type(responses) is not list and type(responses) is not tuple: raise ValueError(error) + # Every valid non-empty two-dimensional built-in matrix with N scalar cells + # visits at most N row entries plus N scalar entries. Keep a separate budget + # so malformed zero-cell/deep fan-out cannot evade the logical-cell ceiling. + structural_nodes = 0 + # Each frame is [exact built-in container, next child index, logical-cell subtotal]. # Memoized subtotals let repeated/shared acyclic subtrees count by logical # occurrence without re-traversing their descendants exponentially. @@ -115,6 +122,9 @@ def _trusted_response_array(responses: object) -> np.ndarray: frame[1] = child_index + 1 child = item[child_index] + structural_nodes += 1 + if structural_nodes > _MAX_CRM_RESPONSE_STRUCTURAL_NODES: + raise ValueError(structural_error) child_type = type(child) if _is_exact_type(child_type, _TRUSTED_RESPONSE_SCALAR_TYPES): From 537af70905e71155c8cc41d7bea8100d2455c9ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 21:29:28 -0700 Subject: [PATCH 3/4] test(crm): prove valid matrix fits structural budget --- tests/test_crm_response_admission.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_crm_response_admission.py b/tests/test_crm_response_admission.py index 9a1c2a41e..dfb9a2d09 100644 --- a/tests/test_crm_response_admission.py +++ b/tests/test_crm_response_admission.py @@ -151,7 +151,7 @@ def unexpected_asarray(*args, **kwargs): def unexpected_core() -> object: raise AssertionError("compiled core discovered before CRM structural admission") - monkeypatch.setattr(crm, "_MAX_CRM_RESPONSE_STRUCTURAL_NODES", 8, raising=False) + monkeypatch.setattr(crm, "_MAX_CRM_RESPONSE_STRUCTURAL_NODES", 8) monkeypatch.setattr(crm.np, "asarray", unexpected_asarray) monkeypatch.setattr(fitstats, "_core_module", unexpected_core) @@ -159,6 +159,29 @@ def unexpected_core() -> object: crm.fit_crm(responses) +def test_valid_builtin_matrix_fits_tight_structural_budget(monkeypatch): + """The 2N structural envelope preserves every valid non-empty 2-D matrix.""" + + captured: dict[str, tuple[object, ...]] = {} + + class CapturingCore: + def fit_crm(self, *args): + captured["args"] = args + return _result() + + monkeypatch.setattr(crm, "_MAX_CRM_RESPONSE_CELLS", 2) + monkeypatch.setattr(crm, "_MAX_CRM_RESPONSE_STRUCTURAL_NODES", 4) + monkeypatch.setattr(fitstats, "_core_module", lambda: CapturingCore()) + + fitted = crm.fit_crm([[0.25], [0.75]], max_iter=1) + + args = captured["args"] + np.testing.assert_array_equal(args[0], np.array([0.25, 0.75], dtype=np.float64)) + np.testing.assert_array_equal(args[1], np.array([True, True], dtype=bool)) + assert args[2:4] == (2, 1) + assert fitted.n_parameters == 3 + + def test_builtin_response_matrix_preserves_trusted_numpy_scalar_marshalling(monkeypatch): """Ordinary built-in rows with concrete NumPy reals remain compatible.""" From b4e00a17f7a9ad9efda16fd5aae791950c3fd579 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 21:29:51 -0700 Subject: [PATCH 4/4] docs(changelog): record CRM traversal budget --- docs/changelog.d/1366-crm-structural-budget.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/changelog.d/1366-crm-structural-budget.md diff --git a/docs/changelog.d/1366-crm-structural-budget.md b/docs/changelog.d/1366-crm-structural-budget.md new file mode 100644 index 000000000..3825da2e5 --- /dev/null +++ b/docs/changelog.d/1366-crm-structural-budget.md @@ -0,0 +1,7 @@ +# Bound CRM response traversal before NumPy materialization + +## Fixed + +- Continuous Response Model response admission now applies an independent structural-work ceiling while traversing exact built-in list/tuple evidence, so malformed zero-cell or deeply nested container fan-out cannot consume unbounded Python work before NumPy materialization. +- The structural ceiling is `2 ×` the existing 20,000,000 logical-cell envelope, which preserves every valid non-empty two-dimensional persons-by-items built-in matrix while bounding malformed container-only traversal. +- Existing callback-free cycle rejection, shared acyclic subtree handling, exact NumPy row compatibility, logical-cell accounting, NaN-only missingness, and Rust-owned Samejima CRM likelihood/EM/EAP arithmetic remain unchanged. \ No newline at end of file